more resilient implementation of Moodle course API access
This commit is contained in:
parent
b74778a67d
commit
bbf241b08e
7 changed files with 394 additions and 88 deletions
|
@ -10,11 +10,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
@ -72,14 +70,7 @@ public interface LmsAPITemplate {
|
||||||
*
|
*
|
||||||
* @param ids the Set of Quiz identifiers to get the QuizData for
|
* @param ids the Set of Quiz identifiers to get the QuizData for
|
||||||
* @return Collection of all QuizData from the given id set */
|
* @return Collection of all QuizData from the given id set */
|
||||||
default Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
|
Collection<Result<QuizData>> getQuizzes(Set<String> ids);
|
||||||
return getQuizzes(new FilterMap())
|
|
||||||
.getOrElse(Collections::emptyList)
|
|
||||||
.stream()
|
|
||||||
.filter(quiz -> ids.contains(quiz.id))
|
|
||||||
.map(Result::of)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get all QuizData for the set of QuizData identifiers from LMS API in a collection
|
/** Get all QuizData for the set of QuizData identifiers from LMS API in a collection
|
||||||
* of Result. If particular Quiz cannot be loaded because of errors or deletion,
|
* of Result. If particular Quiz cannot be loaded because of errors or deletion,
|
||||||
|
|
|
@ -37,6 +37,7 @@ public abstract class CourseAccess {
|
||||||
private static final Logger log = LoggerFactory.getLogger(CourseAccess.class);
|
private static final Logger log = LoggerFactory.getLogger(CourseAccess.class);
|
||||||
|
|
||||||
protected final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest;
|
protected final MemoizingCircuitBreaker<List<QuizData>> allQuizzesRequest;
|
||||||
|
protected final CircuitBreaker<List<QuizData>> quizzesRequest;
|
||||||
protected final CircuitBreaker<Chapters> chaptersRequest;
|
protected final CircuitBreaker<Chapters> chaptersRequest;
|
||||||
protected final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest;
|
protected final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest;
|
||||||
|
|
||||||
|
@ -49,6 +50,11 @@ public abstract class CourseAccess {
|
||||||
true,
|
true,
|
||||||
Constants.HOUR_IN_MILLIS);
|
Constants.HOUR_IN_MILLIS);
|
||||||
|
|
||||||
|
this.quizzesRequest = asyncService.createCircuitBreaker(
|
||||||
|
3,
|
||||||
|
Constants.MINUTE_IN_MILLIS,
|
||||||
|
Constants.MINUTE_IN_MILLIS);
|
||||||
|
|
||||||
this.chaptersRequest = asyncService.createCircuitBreaker(
|
this.chaptersRequest = asyncService.createCircuitBreaker(
|
||||||
3,
|
3,
|
||||||
Constants.SECOND_IN_MILLIS * 10,
|
Constants.SECOND_IN_MILLIS * 10,
|
||||||
|
@ -72,22 +78,29 @@ public abstract class CourseAccess {
|
||||||
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
|
public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final List<QuizData> cached = this.allQuizzesRequest.getCached();
|
final List<QuizData> cached = this.allQuizzesRequest.getCached();
|
||||||
if (cached == null) {
|
final List<QuizData> available = (cached != null)
|
||||||
throw new RuntimeException("No cached quizzes");
|
? cached
|
||||||
}
|
: quizzesSupplier(ids).get();
|
||||||
|
|
||||||
final Map<String, QuizData> cacheMapping = cached
|
final Map<String, QuizData> quizMapping = available
|
||||||
.stream()
|
.stream()
|
||||||
.collect(Collectors.toMap(q -> q.id, Function.identity()));
|
.collect(Collectors.toMap(q -> q.id, Function.identity()));
|
||||||
|
|
||||||
if (!cacheMapping.keySet().containsAll(ids)) {
|
if (!quizMapping.keySet().containsAll(ids)) {
|
||||||
throw new RuntimeException("Not all requested quizzes cached");
|
|
||||||
|
final Map<String, QuizData> collect = quizzesSupplier(ids).get()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
|
||||||
|
if (collect != null) {
|
||||||
|
quizMapping.clear();
|
||||||
|
quizMapping.putAll(collect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ids
|
return ids
|
||||||
.stream()
|
.stream()
|
||||||
.map(id -> {
|
.map(id -> {
|
||||||
final QuizData q = cacheMapping.get(id);
|
final QuizData q = quizMapping.get(id);
|
||||||
return (q == null)
|
return (q == null)
|
||||||
? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id))
|
? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id))
|
||||||
: Result.of(q);
|
: Result.of(q);
|
||||||
|
@ -96,6 +109,11 @@ public abstract class CourseAccess {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Note: this can be overwritten to load missing requested quiz data from specified LMS by id */
|
||||||
|
protected Map<String, QuizData> loadMissingData(final Set<String> ids) {
|
||||||
|
throw new RuntimeException("Not all requested quizzes cached");
|
||||||
|
}
|
||||||
|
|
||||||
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||||
return this.allQuizzesRequest.get()
|
return this.allQuizzesRequest.get()
|
||||||
.map(LmsAPIService.quizzesFilterFunction(filterMap));
|
.map(LmsAPIService.quizzesFilterFunction(filterMap));
|
||||||
|
@ -130,6 +148,8 @@ public abstract class CourseAccess {
|
||||||
Collections.emptyMap());
|
Collections.emptyMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids);
|
||||||
|
|
||||||
protected abstract Supplier<List<QuizData>> allQuizzesSupplier();
|
protected abstract Supplier<List<QuizData>> allQuizzesSupplier();
|
||||||
|
|
||||||
protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId);
|
protected abstract Supplier<Chapters> getCourseChaptersSupplier(final String courseId);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -164,6 +165,13 @@ final class OpenEdxCourseAccess extends CourseAccess {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
|
||||||
|
return () -> getRestTemplate()
|
||||||
|
.map(template -> this.collectQuizzes(template, ids))
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<List<QuizData>> allQuizzesSupplier() {
|
protected Supplier<List<QuizData>> allQuizzesSupplier() {
|
||||||
return () -> getRestTemplate()
|
return () -> getRestTemplate()
|
||||||
|
@ -187,6 +195,25 @@ final class OpenEdxCourseAccess extends CourseAccess {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ArrayList<QuizData> collectQuizzes(final OAuth2RestTemplate restTemplate, final Set<String> ids) {
|
||||||
|
final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup);
|
||||||
|
return collectCourses(
|
||||||
|
this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
|
||||||
|
restTemplate,
|
||||||
|
ids)
|
||||||
|
.stream()
|
||||||
|
.reduce(
|
||||||
|
new ArrayList<>(),
|
||||||
|
(list, courseData) -> {
|
||||||
|
list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI));
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
(list1, list2) -> {
|
||||||
|
list1.addAll(list2);
|
||||||
|
return list1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private ArrayList<QuizData> collectAllQuizzes(final OAuth2RestTemplate restTemplate) {
|
private ArrayList<QuizData> collectAllQuizzes(final OAuth2RestTemplate restTemplate) {
|
||||||
final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup);
|
final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup);
|
||||||
return collectAllCourses(
|
return collectAllCourses(
|
||||||
|
@ -229,6 +256,29 @@ final class OpenEdxCourseAccess extends CourseAccess {
|
||||||
return _externalStartURI;
|
return _externalStartURI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<CourseData> collectCourses(
|
||||||
|
final String pageURI,
|
||||||
|
final OAuth2RestTemplate restTemplate,
|
||||||
|
final Set<String> ids) {
|
||||||
|
|
||||||
|
final List<CourseData> collector = new ArrayList<>();
|
||||||
|
EdXPage page = getEdxPage(pageURI, restTemplate).getBody();
|
||||||
|
if (page != null) {
|
||||||
|
collector.addAll(page.results);
|
||||||
|
while (page != null && StringUtils.isNotBlank(page.next)) {
|
||||||
|
page = getEdxPage(page.next, restTemplate).getBody();
|
||||||
|
if (page != null) {
|
||||||
|
page.results
|
||||||
|
.stream()
|
||||||
|
.filter(cd -> ids.contains(cd.id))
|
||||||
|
.forEach(collector::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collector;
|
||||||
|
}
|
||||||
|
|
||||||
private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) {
|
private List<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) {
|
||||||
final List<CourseData> collector = new ArrayList<>();
|
final List<CourseData> collector = new ArrayList<>();
|
||||||
EdXPage page = getEdxPage(pageURI, restTemplate).getBody();
|
EdXPage page = getEdxPage(pageURI, restTemplate).getBody();
|
||||||
|
|
|
@ -12,7 +12,10 @@ import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -67,6 +70,22 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
return this.openEdxCourseAccess.getQuizzes(filterMap);
|
return this.openEdxCourseAccess.getQuizzes(filterMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
|
||||||
|
final Map<String, QuizData> mapping = this.openEdxCourseAccess
|
||||||
|
.quizzesSupplier(ids)
|
||||||
|
.get()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
|
||||||
|
|
||||||
|
return ids.stream()
|
||||||
|
.map(id -> {
|
||||||
|
final QuizData data = mapping.get(id);
|
||||||
|
return (data == null) ? Result.<QuizData> ofRuntimeError("Missing id: " + id) : Result.of(data);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<QuizData> getQuizFromCache(final String id) {
|
public Result<QuizData> getQuizFromCache(final String id) {
|
||||||
return getQuizzesFromCache(new HashSet<>(Arrays.asList(id)))
|
return getQuizzesFromCache(new HashSet<>(Arrays.asList(id)))
|
||||||
|
|
|
@ -8,11 +8,14 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -28,7 +31,9 @@ import org.springframework.util.MultiValueMap;
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.core.JsonParseException;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||||
|
@ -51,10 +56,20 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
private static final Logger log = LoggerFactory.getLogger(MoodleCourseAccess.class);
|
private static final Logger log = LoggerFactory.getLogger(MoodleCourseAccess.class);
|
||||||
|
|
||||||
private static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id=";
|
private static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id=";
|
||||||
|
|
||||||
private static final String MOODLE_COURSE_API_FUNCTION_NAME = "core_course_get_courses";
|
private static final String MOODLE_COURSE_API_FUNCTION_NAME = "core_course_get_courses";
|
||||||
private static final String MOODLE_USER_PROFILE_API_FUNCTION_NAME = "core_user_get_users_by_field";
|
private static final String MOODLE_USER_PROFILE_API_FUNCTION_NAME = "core_user_get_users_by_field";
|
||||||
private static final String MOODLE_QUIZ_API_FUNCTION_NAME = "mod_quiz_get_quizzes_by_courses";
|
private static final String MOODLE_QUIZ_API_FUNCTION_NAME = "mod_quiz_get_quizzes_by_courses";
|
||||||
private static final String MOODLE_COURSE_API_COURSE_IDS = "courseids";
|
private static final String MOODLE_COURSE_API_COURSE_IDS = "courseids";
|
||||||
|
private static final String MOODLE_COURSE_API_IDS = "ids";
|
||||||
|
private static final String MOODLE_COURSE_SEARCH_API_FUNCTION_NAME = "core_course_search_courses";
|
||||||
|
private static final String MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME = "core_course_get_courses_by_field";
|
||||||
|
private static final String MOODLE_COURSE_API_FIELD_NAME = "field";
|
||||||
|
private static final String MOODLE_COURSE_API_FIELD_VALUE = "value";
|
||||||
|
private static final String MOODLE_COURSE_API_SEARCH_CRITERIA_NAME = "criterianame";
|
||||||
|
private static final String MOODLE_COURSE_API_SEARCH_CRITERIA_VALUE = "criteriavalue";
|
||||||
|
private static final String MOODLE_COURSE_API_SEARCH_PAGE = "page";
|
||||||
|
private static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage";
|
||||||
|
|
||||||
private final JSONMapper jsonMapper;
|
private final JSONMapper jsonMapper;
|
||||||
private final LmsSetup lmsSetup;
|
private final LmsSetup lmsSetup;
|
||||||
|
@ -150,6 +165,14 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
return LmsSetupTestResult.ofOkay();
|
return LmsSetupTestResult.ofOkay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
|
||||||
|
return () -> getRestTemplate()
|
||||||
|
.map(template -> getQuizzesForIds(template, ids))
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<List<QuizData>> allQuizzesSupplier() {
|
protected Supplier<List<QuizData>> allQuizzesSupplier() {
|
||||||
return () -> getRestTemplate()
|
return () -> getRestTemplate()
|
||||||
|
@ -166,7 +189,7 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||||
? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||||
: this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
: this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||||
return collectAllCourses(restTemplate)
|
return getAllQuizzes(restTemplate)
|
||||||
.stream()
|
.stream()
|
||||||
.reduce(
|
.reduce(
|
||||||
new ArrayList<>(),
|
new ArrayList<>(),
|
||||||
|
@ -183,30 +206,13 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<CourseData> collectAllCourses(final MoodleAPIRestTemplate restTemplate) {
|
private List<CourseData> getAllQuizzes(final MoodleAPIRestTemplate restTemplate) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// first get courses from Moodle per page
|
||||||
|
final Map<String, CourseData> courseData = new HashMap<>();
|
||||||
|
|
||||||
// first get courses from Moodle...
|
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, 0, 1000);
|
||||||
final String coursesJSON = restTemplate.callMoodleAPIFunction(MOODLE_COURSE_API_FUNCTION_NAME);
|
courseData.putAll(coursesPage.stream().collect(Collectors.toMap(cd -> cd.id, Function.identity())));
|
||||||
Map<String, CourseData> courseData = this.jsonMapper.<Collection<CourseData>> readValue(
|
|
||||||
coursesJSON,
|
|
||||||
new TypeReference<Collection<CourseData>>() {
|
|
||||||
})
|
|
||||||
.stream()
|
|
||||||
.collect(Collectors.toMap(d -> d.id, Function.identity()));
|
|
||||||
|
|
||||||
if (courseData.size() > 100) {
|
|
||||||
log.warn(
|
|
||||||
"Got more then 100 courses form Moodle: size {}. Trim it to the latest 100 courses",
|
|
||||||
courseData.size());
|
|
||||||
final long nowInMillis = DateTime.now(DateTimeZone.UTC).getMillis();
|
|
||||||
courseData = courseData.values().stream()
|
|
||||||
.filter(cd -> cd.end_date != null && cd.end_date.longValue() < nowInMillis)
|
|
||||||
.sorted((cd1, cd2) -> cd1.start_date.compareTo(cd2.start_date))
|
|
||||||
.limit(100)
|
|
||||||
.collect(Collectors.toMap(cd -> cd.id, Function.identity()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// then get all quizzes of courses and filter
|
// then get all quizzes of courses and filter
|
||||||
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||||
|
@ -234,7 +240,154 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
.filter(c -> !c.quizzes.isEmpty())
|
.filter(c -> !c.quizzes.isEmpty())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new RuntimeException("Unexpected exception while trying to get course data: ", e);
|
log.error("Unexpected exception while trying to get course data: ", e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<CourseData> getCoursesPage(
|
||||||
|
final MoodleAPIRestTemplate restTemplate,
|
||||||
|
final int page,
|
||||||
|
final int size) throws JsonParseException, JsonMappingException, IOException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
final long aYearAgo = DateTime.now(DateTimeZone.UTC).minusYears(1).getMillis();
|
||||||
|
// get course ids per page
|
||||||
|
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||||
|
attributes.add(MOODLE_COURSE_API_SEARCH_CRITERIA_NAME, "search");
|
||||||
|
attributes.add(MOODLE_COURSE_API_SEARCH_CRITERIA_VALUE, "");
|
||||||
|
attributes.add(MOODLE_COURSE_API_SEARCH_PAGE, String.valueOf(page));
|
||||||
|
attributes.add(MOODLE_COURSE_API_SEARCH_PAGE_SIZE, String.valueOf(size));
|
||||||
|
|
||||||
|
final String courseKeyPageJSON = restTemplate.callMoodleAPIFunction(
|
||||||
|
MOODLE_COURSE_SEARCH_API_FUNCTION_NAME,
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
final CoursePage keysPage = this.jsonMapper.readValue(
|
||||||
|
courseKeyPageJSON,
|
||||||
|
CoursePage.class);
|
||||||
|
|
||||||
|
// get courses
|
||||||
|
final Set<String> ids = keysPage.courseKeys
|
||||||
|
.stream()
|
||||||
|
.map(key -> key.id)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
|
||||||
|
return getCoursesForIds(restTemplate, ids)
|
||||||
|
.stream()
|
||||||
|
.filter(course -> course.time_created == null
|
||||||
|
|| course.time_created.longValue() > aYearAgo
|
||||||
|
|| (course.end_date == null
|
||||||
|
|| (course.end_date <= 0
|
||||||
|
|| course.end_date > now)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.error("Unexpected error while trying to get courses page: ", e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QuizData> getQuizzesForIds(
|
||||||
|
final MoodleAPIRestTemplate restTemplate,
|
||||||
|
final Set<String> quizIds) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Get quizzes for ids: {}", quizIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, CourseData> courseData = getCoursesForIds(
|
||||||
|
restTemplate,
|
||||||
|
quizIds.stream()
|
||||||
|
.map(MoodleCourseAccess::getCourseId)
|
||||||
|
.collect(Collectors.toSet()))
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(cd -> cd.id, Function.identity()));
|
||||||
|
|
||||||
|
final List<String> courseIds = new ArrayList<>(courseData.keySet());
|
||||||
|
if (courseIds.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
if (courseIds.size() == 1) {
|
||||||
|
// NOTE: This is a workaround because the Moodle API do not support lists with only one element.
|
||||||
|
courseIds.add("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// then get all quizzes of courses and filter
|
||||||
|
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||||
|
attributes.put(MOODLE_COURSE_API_COURSE_IDS, courseIds);
|
||||||
|
|
||||||
|
final String quizzesJSON = restTemplate.callMoodleAPIFunction(
|
||||||
|
MOODLE_QUIZ_API_FUNCTION_NAME,
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
|
||||||
|
quizzesJSON,
|
||||||
|
CourseQuizData.class);
|
||||||
|
|
||||||
|
final Map<String, CourseData> finalCourseDataRef = courseData;
|
||||||
|
courseQuizData.quizzes
|
||||||
|
.forEach(quiz -> {
|
||||||
|
final CourseData course = finalCourseDataRef.get(quiz.course);
|
||||||
|
if (course != null) {
|
||||||
|
course.quizzes.add(quiz);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final String urlPrefix = (this.lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
|
||||||
|
? this.lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
|
||||||
|
: this.lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
|
||||||
|
|
||||||
|
return courseData.values()
|
||||||
|
.stream()
|
||||||
|
.filter(c -> !c.quizzes.isEmpty())
|
||||||
|
.reduce(
|
||||||
|
new ArrayList<>(),
|
||||||
|
(list, cd) -> {
|
||||||
|
list.addAll(quizDataOf(
|
||||||
|
this.lmsSetup,
|
||||||
|
cd,
|
||||||
|
urlPrefix));
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
(list1, list2) -> {
|
||||||
|
list1.addAll(list2);
|
||||||
|
return list1;
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.error("Unexpected error while trying to get quizzes for ids", e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<CourseData> getCoursesForIds(
|
||||||
|
final MoodleAPIRestTemplate restTemplate,
|
||||||
|
final Set<String> ids) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Get courses for ids: {}", ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String joinedIds = StringUtils.join(ids, Constants.COMMA);
|
||||||
|
|
||||||
|
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
|
||||||
|
attributes.add(MOODLE_COURSE_API_FIELD_NAME, MOODLE_COURSE_API_IDS);
|
||||||
|
attributes.add(MOODLE_COURSE_API_FIELD_VALUE, joinedIds);
|
||||||
|
final String coursePageJSON = restTemplate.callMoodleAPIFunction(
|
||||||
|
MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME,
|
||||||
|
attributes);
|
||||||
|
|
||||||
|
return this.jsonMapper.<Courses> readValue(
|
||||||
|
coursePageJSON,
|
||||||
|
Courses.class).courses;
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.error("Unexpected error while trying to get courses for ids", e);
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +412,11 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
final String startURI = uriPrefix + courseQuizData.course_module;
|
final String startURI = uriPrefix + courseQuizData.course_module;
|
||||||
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit));
|
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_TIME_LIMIT, String.valueOf(courseQuizData.time_limit));
|
||||||
return new QuizData(
|
return new QuizData(
|
||||||
getInternalQuizId(courseQuizData.course_module, courseData.short_name, courseData.idnumber),
|
getInternalQuizId(
|
||||||
|
courseQuizData.course_module,
|
||||||
|
courseData.id,
|
||||||
|
courseData.short_name,
|
||||||
|
courseData.idnumber),
|
||||||
lmsSetup.getInstitutionId(),
|
lmsSetup.getInstitutionId(),
|
||||||
lmsSetup.id,
|
lmsSetup.id,
|
||||||
lmsSetup.getLmsType(),
|
lmsSetup.getLmsType(),
|
||||||
|
@ -289,6 +446,93 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
return Result.of(this.restTemplate);
|
return Result.of(this.restTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final String getInternalQuizId(
|
||||||
|
final String quizId,
|
||||||
|
final String courseId,
|
||||||
|
final String shortname,
|
||||||
|
final String idnumber) {
|
||||||
|
|
||||||
|
return StringUtils.join(
|
||||||
|
new String[] {
|
||||||
|
quizId,
|
||||||
|
courseId,
|
||||||
|
StringUtils.isNotBlank(shortname) ? shortname : Constants.EMPTY_NOTE,
|
||||||
|
StringUtils.isNotBlank(idnumber) ? idnumber : Constants.EMPTY_NOTE
|
||||||
|
},
|
||||||
|
Constants.COLON);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String getQuizId(final String internalQuizId) {
|
||||||
|
if (StringUtils.isBlank(internalQuizId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringUtils.split(internalQuizId, Constants.COLON)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String getCourseId(final String internalQuizId) {
|
||||||
|
if (StringUtils.isBlank(internalQuizId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringUtils.split(internalQuizId, Constants.COLON)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String getShortname(final String internalQuizId) {
|
||||||
|
if (StringUtils.isBlank(internalQuizId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String[] split = StringUtils.split(internalQuizId, Constants.COLON);
|
||||||
|
if (split.length < 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String shortName = split[2];
|
||||||
|
return shortName.equals(Constants.EMPTY_NOTE) ? null : shortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String getIdnumber(final String internalQuizId) {
|
||||||
|
if (StringUtils.isBlank(internalQuizId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String[] split = StringUtils.split(internalQuizId, Constants.COLON);
|
||||||
|
if (split.length < 4) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String idNumber = split[3];
|
||||||
|
return idNumber.equals(Constants.EMPTY_NOTE) ? null : idNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Mapping Classes ---
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
static final class CoursePage {
|
||||||
|
final Collection<CourseKey> courseKeys;
|
||||||
|
|
||||||
|
public CoursePage(
|
||||||
|
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys) {
|
||||||
|
|
||||||
|
this.courseKeys = courseKeys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
static final class CourseKey {
|
||||||
|
final String id;
|
||||||
|
final String short_name;
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
protected CourseKey(
|
||||||
|
@JsonProperty(value = "id") final String id,
|
||||||
|
@JsonProperty(value = "shortname") final String short_name) {
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.short_name = short_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Maps the Moodle course API course data */
|
/** Maps the Moodle course API course data */
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
static final class CourseData {
|
static final class CourseData {
|
||||||
|
@ -328,51 +572,14 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final String getInternalQuizId(final String quizId, final String shortname, final String idnumber) {
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
final StringBuilder sb = new StringBuilder(quizId);
|
static final class Courses {
|
||||||
if (StringUtils.isNotEmpty(shortname)) {
|
final Collection<CourseData> courses;
|
||||||
sb.insert(0, ":").insert(0, shortname);
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotEmpty(idnumber)) {
|
|
||||||
sb.insert(0, ":").insert(0, idnumber);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String getQuizId(final String internalQuizId) {
|
@JsonCreator
|
||||||
if (StringUtils.isBlank(internalQuizId)) {
|
protected Courses(
|
||||||
return null;
|
@JsonProperty(value = "courses") final Collection<CourseData> courses) {
|
||||||
}
|
this.courses = courses;
|
||||||
|
|
||||||
final String[] ids = StringUtils.split(internalQuizId, Constants.COLON);
|
|
||||||
return ids[ids.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String getShortname(final String internalQuizId) {
|
|
||||||
if (StringUtils.isBlank(internalQuizId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String[] ids = StringUtils.split(internalQuizId, Constants.COLON);
|
|
||||||
if (ids.length == 3) {
|
|
||||||
return ids[1];
|
|
||||||
} else if (ids.length == 2) {
|
|
||||||
return ids[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String getIdnumber(final String internalQuizId) {
|
|
||||||
if (StringUtils.isBlank(internalQuizId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String[] ids = StringUtils.split(internalQuizId, Constants.COLON);
|
|
||||||
if (ids.length == 3) {
|
|
||||||
return ids[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,10 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -65,6 +68,22 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
|
||||||
return this.moodleCourseAccess.getQuizzes(filterMap);
|
return this.moodleCourseAccess.getQuizzes(filterMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
|
||||||
|
final Map<String, QuizData> mapping = this.moodleCourseAccess
|
||||||
|
.quizzesSupplier(ids)
|
||||||
|
.get()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(qd -> qd.id, Function.identity()));
|
||||||
|
|
||||||
|
return ids.stream()
|
||||||
|
.map(id -> {
|
||||||
|
final QuizData data = mapping.get(id);
|
||||||
|
return (data == null) ? Result.<QuizData> ofRuntimeError("Missing id: " + id) : Result.of(data);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
|
public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
|
||||||
return this.moodleCourseAccess.getQuizzesFromCache(ids)
|
return this.moodleCourseAccess.getQuizzesFromCache(ids)
|
||||||
|
|
|
@ -10,9 +10,9 @@ sebserver.gui.webservice.apipath=/admin-api/v1
|
||||||
# defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page
|
# defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page
|
||||||
sebserver.gui.webservice.poll-interval=1000
|
sebserver.gui.webservice.poll-interval=1000
|
||||||
|
|
||||||
sebserver.gui.webservice.mock-lms-enabled=false
|
sebserver.gui.webservice.mock-lms-enabled=true
|
||||||
sebserver.gui.webservice.edx-lms-enabled=true
|
sebserver.gui.webservice.edx-lms-enabled=true
|
||||||
sebserver.gui.webservice.moodle-lms-enabled=false
|
sebserver.gui.webservice.moodle-lms-enabled=true
|
||||||
|
|
||||||
|
|
||||||
sebserver.gui.theme=css/sebserver.css
|
sebserver.gui.theme=css/sebserver.css
|
||||||
|
|
Loading…
Add table
Reference in a new issue