better Moodle course access (background fetch and caching)

This commit is contained in:
anhefti 2020-12-17 13:20:08 +01:00
parent 5aca3bc5b5
commit 3cefcbe3f3
6 changed files with 512 additions and 233 deletions

View file

@ -516,6 +516,10 @@ public final class Utils {
return DateTime.now(DateTimeZone.UTC).getMillis();
}
public static long getSecondsNow() {
return DateTime.now(DateTimeZone.UTC).getMillis() / 1000;
}
public static RGB toRGB(final String rgbString) {
if (StringUtils.isNotBlank(rgbString)) {
return new RGB(

View file

@ -8,7 +8,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -17,13 +16,10 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
@ -32,9 +28,7 @@ import org.springframework.util.MultiValueMap;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParseException;
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.api.JSONMapper;
@ -44,7 +38,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Pair;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess;
@ -59,23 +52,24 @@ public class MoodleCourseAccess extends CourseAccess {
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_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_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";
static final String MOODLE_COURSE_API_FUNCTION_NAME = "core_course_get_courses";
static final String MOODLE_USER_PROFILE_API_FUNCTION_NAME = "core_user_get_users_by_field";
static final String MOODLE_QUIZ_API_FUNCTION_NAME = "mod_quiz_get_quizzes_by_courses";
static final String MOODLE_COURSE_API_COURSE_IDS = "courseids";
static final String MOODLE_COURSE_API_IDS = "ids";
static final String MOODLE_COURSE_SEARCH_API_FUNCTION_NAME = "core_course_search_courses";
static final String MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME = "core_course_get_courses_by_field";
static final String MOODLE_COURSE_API_FIELD_NAME = "field";
static final String MOODLE_COURSE_API_FIELD_VALUE = "value";
static final String MOODLE_COURSE_API_SEARCH_CRITERIA_NAME = "criterianame";
static final String MOODLE_COURSE_API_SEARCH_CRITERIA_VALUE = "criteriavalue";
static final String MOODLE_COURSE_API_SEARCH_PAGE = "page";
static final String MOODLE_COURSE_API_SEARCH_PAGE_SIZE = "perpage";
private final JSONMapper jsonMapper;
private final LmsSetup lmsSetup;
private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private final MoodleCourseDataLazyLoader moodleCourseDataLazyLoader;
private MoodleAPIRestTemplate restTemplate;
@ -83,11 +77,13 @@ public class MoodleCourseAccess extends CourseAccess {
final JSONMapper jsonMapper,
final LmsSetup lmsSetup,
final MoodleRestTemplateFactory moodleRestTemplateFactory,
final MoodleCourseDataLazyLoader moodleCourseDataLazyLoader,
final AsyncService asyncService) {
super(asyncService);
this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup;
this.moodleCourseDataLazyLoader = moodleCourseDataLazyLoader;
this.moodleRestTemplateFactory = moodleRestTemplateFactory;
}
@ -187,12 +183,43 @@ public class MoodleCourseAccess extends CourseAccess {
throw new UnsupportedOperationException("not available yet");
}
private ArrayList<QuizData> collectAllQuizzes(final MoodleAPIRestTemplate restTemplate) {
private List<QuizData> collectAllQuizzes(final MoodleAPIRestTemplate restTemplate) {
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 getAllQuizzes(restTemplate)
Collection<CourseData> courseQuizData = Collections.emptyList();
if (this.moodleCourseDataLazyLoader.isRunning()) {
courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds();
} else if (this.moodleCourseDataLazyLoader.getLastRunTime() <= 0) {
// first run async and wait some time, get what is there
this.moodleCourseDataLazyLoader.loadAsync(restTemplate);
try {
Thread.sleep(5 * Constants.SECOND_IN_MILLIS);
courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds();
} catch (final Exception e) {
log.error("Failed to wait for first load run: ", e);
return Collections.emptyList();
}
} else if (this.moodleCourseDataLazyLoader.isLongRunningTask()) {
// kick off the task again when old asynchronously and take back what is there instantly
if (Utils.getMillisecondsNow() - this.moodleCourseDataLazyLoader.getLastRunTime() > 10
* Constants.MINUTE_IN_MILLIS) {
this.moodleCourseDataLazyLoader.loadAsync(restTemplate);
}
courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds();
} else {
// just run the task in sync
this.moodleCourseDataLazyLoader.loadSync(restTemplate);
courseQuizData = this.moodleCourseDataLazyLoader.getPreFilteredCourseIds();
}
if (courseQuizData.isEmpty()) {
return Collections.emptyList();
}
return courseQuizData
.stream()
.reduce(
new ArrayList<>(),
@ -209,169 +236,6 @@ public class MoodleCourseAccess extends CourseAccess {
});
}
private List<CourseData> getAllQuizzes(final MoodleAPIRestTemplate restTemplate) {
final List<CourseData> result = new ArrayList<>();
int page = 0;
Pair<List<CourseData>, Integer> quizzesBatch = getQuizzesBatch(restTemplate, page);
result.addAll(quizzesBatch.a);
log.info("Got quiz page batch for page {} of size {} with {} items",
page,
quizzesBatch.b,
quizzesBatch.a.size());
if (quizzesBatch.b != null && quizzesBatch.b > 0) {
page++;
quizzesBatch = getQuizzesBatch(restTemplate, page);
result.addAll(quizzesBatch.a);
log.info("Got quiz page batch for page {} of size {} with {} items",
page,
quizzesBatch.b,
quizzesBatch.a.size());
}
// while (quizzesBatch.b != null && quizzesBatch.b > 0) {
// page++;
// quizzesBatch = getQuizzesBatch(restTemplate, page);
// result.addAll(quizzesBatch.a);
//
// log.info("Got quiz page batch for page {} of size {} with {} items",
// page,
// quizzesBatch.b,
// quizzesBatch.a.size());
// }
return result;
}
private Pair<List<CourseData>, Integer> getQuizzesBatch(
final MoodleAPIRestTemplate restTemplate,
final int page) {
try {
// first get courses from Moodle for page
final Map<String, CourseData> courseData = new HashMap<>();
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, page, 1000);
if (coursesPage.isEmpty()) {
return new Pair<>(Collections.emptyList(), 0);
}
courseData.putAll(coursesPage
.stream()
.collect(Collectors.toMap(cd -> cd.id, Function.identity())));
// then get all quizzes of courses and filter
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
attributes.put(MOODLE_COURSE_API_COURSE_IDS, new ArrayList<>(courseData.keySet()));
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;
if (courseQuizData.quizzes != null) {
courseQuizData.quizzes
.stream()
.filter(getQuizFilter())
.forEach(quiz -> {
final CourseData course = finalCourseDataRef.get(quiz.course);
if (course != null) {
course.quizzes.add(quiz);
}
});
}
return new Pair<>(courseData.values()
.stream()
.filter(c -> !c.quizzes.isEmpty())
.collect(Collectors.toList()),
coursesPage.size());
} catch (final Exception e) {
log.error("Unexpected exception while trying to get course data: ", e);
return new Pair<>(Collections.emptyList(), 0);
}
}
private Predicate<CourseQuiz> getQuizFilter() {
final long now = DateTime.now(DateTimeZone.UTC).getMillis() / 1000;
return quiz -> {
if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) {
return true;
}
log.info("remove quiz {} end_time {} now {}", quiz.name, quiz.time_close, now);
return false;
};
}
private Predicate<CourseData> getCourseFilter() {
final long now = DateTime.now(DateTimeZone.UTC).getMillis() / 1000;
return course -> {
if (course.end_date == null || course.end_date == 0 || course.end_date > now) {
return true;
}
log.info("remove course {} end_time {} now {}", course.short_name, course.end_date, now);
return false;
};
}
private Collection<CourseData> getCoursesPage(
final MoodleAPIRestTemplate restTemplate,
final int page,
final int size) throws JsonParseException, JsonMappingException, IOException {
try {
// 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);
if (keysPage == null || keysPage.courseKeys == null || keysPage.courseKeys.isEmpty()) {
log.info("No courses found on page: {}", page);
return Collections.emptyList();
}
// get courses
final Set<String> ids = keysPage.courseKeys
.stream()
.map(key -> key.id)
.collect(Collectors.toSet());
// return getCoursesForIds(restTemplate, ids);
final Collection<CourseData> result = getCoursesForIds(restTemplate, ids)
.stream()
.filter(getCourseFilter())
.collect(Collectors.toList());
// log.info("course page with {} courses, after filtering {} left", keysPage.courseKeys, result.size());
return result;
} 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) {
@ -595,54 +459,6 @@ public class MoodleCourseAccess extends CourseAccess {
// ---- 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;
final String category_name;
final String sort_order;
@JsonCreator
protected CourseKey(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "shortname") final String short_name,
@JsonProperty(value = "categoryname") final String category_name,
@JsonProperty(value = "sortorder") final String sort_order) {
this.id = id;
this.short_name = short_name;
this.category_name = category_name;
this.sort_order = sort_order;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("CourseKey [id=");
builder.append(this.id);
builder.append(", short_name=");
builder.append(this.short_name);
builder.append(", category_name=");
builder.append(this.category_name);
builder.append(", sort_order=");
builder.append(this.sort_order);
builder.append("]");
return builder.toString();
}
}
/** Maps the Moodle course API course data */
@JsonIgnoreProperties(ignoreUnknown = true)
static final class CourseData {
@ -680,6 +496,30 @@ public class MoodleCourseAccess extends CourseAccess {
this.time_created = time_created;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final CourseData other = (CourseData) obj;
if (this.id == null) {
if (other.id != null)
return false;
} else if (!this.id.equals(other.id))
return false;
return true;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)

View file

@ -0,0 +1,423 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncRunner;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.CourseData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.CourseQuiz;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.CourseQuizData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess.Courses;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
@Lazy
@Component
@WebServiceProfile
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MoodleCourseDataLazyLoader {
private static final Logger log = LoggerFactory.getLogger(MoodleCourseDataLazyLoader.class);
private final JSONMapper jsonMapper;
private final AsyncRunner asyncRunner;
private final Set<CourseData> preFilteredCourseIds = new HashSet<>();
private long lastRunTime = 0;
private long lastLoadTime = 0;
private boolean running = false;
private final long fromCutTime = DateTime.now(DateTimeZone.UTC).minusYears(3).getMillis() / 1000;
public MoodleCourseDataLazyLoader(
final JSONMapper jsonMapper,
final AsyncRunner asyncRunner) {
this.jsonMapper = jsonMapper;
this.asyncRunner = asyncRunner;
}
public Set<CourseData> getPreFilteredCourseIds() {
return this.preFilteredCourseIds;
}
public long getLastRunTime() {
return this.lastRunTime;
}
public boolean isRunning() {
return this.running;
}
public boolean isLongRunningTask() {
return this.lastLoadTime > 30 * Constants.SECOND_IN_MILLIS;
}
public Set<CourseData> loadSync(final MoodleAPIRestTemplate restTemplate) {
if (this.running) {
throw new IllegalStateException("Is already running asynchronously");
}
this.running = true;
loadAndCache(restTemplate).run();
this.lastRunTime = Utils.getMillisecondsNow();
log.info("Loaded {} courses synchronously", this.preFilteredCourseIds.size());
return this.preFilteredCourseIds;
}
public void loadAsync(final MoodleAPIRestTemplate restTemplate) {
if (this.running) {
return;
}
this.running = true;
this.asyncRunner.runAsync(loadAndCache(restTemplate));
this.lastRunTime = Utils.getMillisecondsNow();
}
private Runnable loadAndCache(final MoodleAPIRestTemplate restTemplate) {
return () -> {
final long startTime = Utils.getMillisecondsNow();
loadAllQuizzes(restTemplate);
this.lastLoadTime = Utils.getMillisecondsNow() - startTime;
this.running = false;
log.info("Loaded {} courses asynchronously", this.preFilteredCourseIds.size());
};
}
private void loadAllQuizzes(final MoodleAPIRestTemplate restTemplate) {
int page = 0;
while (getQuizzesBatch(restTemplate, page)) {
page++;
}
}
private boolean getQuizzesBatch(
final MoodleAPIRestTemplate restTemplate,
final int page) {
try {
// first get courses from Moodle for page
final Map<String, CourseData> courseData = new HashMap<>();
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, page, 1000);
if (coursesPage == null || coursesPage.isEmpty()) {
return false;
}
courseData.putAll(coursesPage
.stream()
.collect(Collectors.toMap(cd -> cd.id, Function.identity())));
// then get all quizzes of courses and filter
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
attributes.put(
MoodleCourseAccess.MOODLE_COURSE_API_COURSE_IDS,
new ArrayList<>(courseData.keySet()));
final String quizzesJSON = restTemplate.callMoodleAPIFunction(
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
attributes);
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
quizzesJSON,
CourseQuizData.class);
if (courseQuizData == null || courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
return false;
}
if (courseQuizData.quizzes != null) {
courseQuizData.quizzes
.stream()
.filter(getQuizFilter())
.forEach(quiz -> {
final CourseData data = courseData.get(quiz.course);
if (data != null) {
data.quizzes.add(quiz);
}
});
this.preFilteredCourseIds.addAll(
courseData.values().stream()
.filter(c -> !c.quizzes.isEmpty())
.collect(Collectors.toList()));
return true;
} else {
return false;
}
} catch (final Exception e) {
log.error("Unexpected exception while trying to get course data: ", e);
return false;
}
}
private Collection<CourseData> getCoursesPage(
final MoodleAPIRestTemplate restTemplate,
final int page,
final int size) throws JsonParseException, JsonMappingException, IOException {
try {
// get course ids per page
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_CRITERIA_NAME, "search");
attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_CRITERIA_VALUE, "");
attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE, String.valueOf(page));
attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_SEARCH_PAGE_SIZE, String.valueOf(size));
final String courseKeyPageJSON = restTemplate.callMoodleAPIFunction(
MoodleCourseAccess.MOODLE_COURSE_SEARCH_API_FUNCTION_NAME,
attributes);
final CoursePage keysPage = this.jsonMapper.readValue(
courseKeyPageJSON,
CoursePage.class);
if (keysPage == null || keysPage.courseKeys == null || keysPage.courseKeys.isEmpty()) {
log.info("No courses found on page: {}", page);
return Collections.emptyList();
}
// get courses
final Set<String> ids = keysPage.courseKeys
.stream()
.map(key -> key.id)
.collect(Collectors.toSet());
final Collection<CourseData> result = getCoursesForIds(restTemplate, ids)
.stream()
.filter(getCourseFilter())
.collect(Collectors.toList());
// log.info("course page with {} courses, after filtering {} left", keysPage.courseKeys, result.size());
return result;
} catch (final Exception e) {
log.error("Unexpected error while trying to get courses page: ", 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(MoodleCourseAccess.MOODLE_COURSE_API_FIELD_NAME, MoodleCourseAccess.MOODLE_COURSE_API_IDS);
attributes.add(MoodleCourseAccess.MOODLE_COURSE_API_FIELD_VALUE, joinedIds);
final String coursePageJSON = restTemplate.callMoodleAPIFunction(
MoodleCourseAccess.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();
}
}
private Predicate<CourseQuiz> getQuizFilter() {
final long now = Utils.getSecondsNow();
return quiz -> {
if (quiz.time_close == null || quiz.time_close == 0 || quiz.time_close > now) {
return true;
}
log.info("remove quiz {} end_time {} now {}", quiz.name, quiz.time_close, now);
return false;
};
}
private Predicate<CourseData> getCourseFilter() {
final long now = Utils.getSecondsNow();
return course -> {
if (course.start_date < this.fromCutTime) {
return false;
}
if (course.end_date == null || course.end_date == 0 || course.end_date > now) {
return true;
}
log.info("remove course {} end_time {} now {}", course.short_name, course.end_date, now);
return false;
};
}
@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;
final String category_name;
final String sort_order;
@JsonCreator
protected CourseKey(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "shortname") final String short_name,
@JsonProperty(value = "categoryname") final String category_name,
@JsonProperty(value = "sortorder") final String sort_order) {
this.id = id;
this.short_name = short_name;
this.category_name = category_name;
this.sort_order = sort_order;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("CourseKey [id=");
builder.append(this.id);
builder.append(", short_name=");
builder.append(this.short_name);
builder.append(", category_name=");
builder.append(this.category_name);
builder.append(", sort_order=");
builder.append(this.sort_order);
builder.append("]");
return builder.toString();
}
}
// @JsonIgnoreProperties(ignoreUnknown = true)
// static final class CourseKeys {
// final Collection<CourseDataKey> courses;
//
// @JsonCreator
// protected CourseKeys(
// @JsonProperty(value = "courses") final Collection<CourseDataKey> courses) {
// this.courses = courses;
// }
// }
// /** Maps the Moodle course API course data */
// @JsonIgnoreProperties(ignoreUnknown = true)
// static final class CourseDataKey {
// final String id;
// final String short_name;
// final Long start_date; // unix-time seconds UTC
// final Long end_date; // unix-time seconds UTC
// final Long time_created; // unix-time seconds UTC
// final Collection<CourseQuizKey> quizzes = new ArrayList<>();
//
// @JsonCreator
// protected CourseDataKey(
// @JsonProperty(value = "id") final String id,
// @JsonProperty(value = "shortname") final String short_name,
// @JsonProperty(value = "startdate") final Long start_date,
// @JsonProperty(value = "enddate") final Long end_date,
// @JsonProperty(value = "timecreated") final Long time_created) {
//
// this.id = id;
// this.short_name = short_name;
// this.start_date = start_date;
// this.end_date = end_date;
// this.time_created = time_created;
// }
//
// }
// @JsonIgnoreProperties(ignoreUnknown = true)
// static final class CourseQuizKeys {
// final Collection<CourseQuizKey> quizzes;
//
// @JsonCreator
// protected CourseQuizKeys(
// @JsonProperty(value = "quizzes") final Collection<CourseQuizKey> quizzes) {
// this.quizzes = quizzes;
// }
// }
//
// @JsonIgnoreProperties(ignoreUnknown = true)
// static final class CourseQuizKey {
// final String id;
// final String course;
// final String name;
// final Long time_open; // unix-time seconds UTC
// final Long time_close; // unix-time seconds UTC
//
// @JsonCreator
// protected CourseQuizKey(
// @JsonProperty(value = "id") final String id,
// @JsonProperty(value = "course") final String course,
// @JsonProperty(value = "name") final String name,
// @JsonProperty(value = "timeopen") final Long time_open,
// @JsonProperty(value = "timeclose") final Long time_close) {
//
// this.id = id;
// this.course = course;
// this.name = name;
// this.time_open = time_open;
// this.time_close = time_close;
// }
// }
}

View file

@ -33,6 +33,7 @@ public class MoodleLmsAPITemplateFactory {
private final AsyncService asyncService;
private final ClientCredentialService clientCredentialService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final MoodleCourseDataLazyLoader moodleCourseDataLazyLoader;
private final String[] alternativeTokenRequestPaths;
protected MoodleLmsAPITemplateFactory(
@ -40,12 +41,14 @@ public class MoodleLmsAPITemplateFactory {
final AsyncService asyncService,
final ClientCredentialService clientCredentialService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final MoodleCourseDataLazyLoader moodleCourseDataLazyLoader,
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) {
this.jsonMapper = jsonMapper;
this.asyncService = asyncService;
this.clientCredentialService = clientCredentialService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.moodleCourseDataLazyLoader = moodleCourseDataLazyLoader;
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR)
: null;
@ -71,6 +74,7 @@ public class MoodleLmsAPITemplateFactory {
this.jsonMapper,
lmsSetup,
moodleRestTemplateFactory,
this.moodleCourseDataLazyLoader,
this.asyncService);
final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction(

View file

@ -7,3 +7,7 @@ server.tomcat.uri-encoding=UTF-8
logging.level.ch=INFO
sebserver.http.client.connect-timeout=150000
sebserver.http.client.connection-request-timeout=100000
sebserver.http.client.read-timeout=200000

View file

@ -67,6 +67,7 @@ public class MoodleCourseAccessTest {
new JSONMapper(),
null,
moodleRestTemplateFactory,
null,
mock(AsyncService.class));
final String examId = "123";
@ -114,6 +115,7 @@ public class MoodleCourseAccessTest {
new JSONMapper(),
null,
moodleRestTemplateFactory,
null,
mock(AsyncService.class));
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess();
@ -135,6 +137,7 @@ public class MoodleCourseAccessTest {
new JSONMapper(),
null,
moodleRestTemplateFactory,
null,
mock(AsyncService.class));
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess();
@ -155,6 +158,7 @@ public class MoodleCourseAccessTest {
new JSONMapper(),
null,
moodleRestTemplateFactory,
null,
mock(AsyncService.class));
final LmsSetupTestResult initAPIAccess = moodleCourseAccess.initAPIAccess();