SEBSERV-75 allow moodle courses and quizzes to be used as exams

This commit is contained in:
anhefti 2020-08-04 11:40:21 +02:00
parent 640211649c
commit dbaffff5c3
6 changed files with 208 additions and 42 deletions

View file

@ -47,6 +47,7 @@ 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_START_URL_PATH = "course/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";
@ -55,6 +56,7 @@ public class MoodleCourseAccess extends CourseAccess {
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final LmsSetup lmsSetup; private final LmsSetup lmsSetup;
private final MoodleRestTemplateFactory moodleRestTemplateFactory; private final MoodleRestTemplateFactory moodleRestTemplateFactory;
private final boolean includeCourses;
private MoodleAPIRestTemplate restTemplate; private MoodleAPIRestTemplate restTemplate;
@ -62,12 +64,14 @@ public class MoodleCourseAccess extends CourseAccess {
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final LmsSetup lmsSetup, final LmsSetup lmsSetup,
final MoodleRestTemplateFactory moodleRestTemplateFactory, final MoodleRestTemplateFactory moodleRestTemplateFactory,
final AsyncService asyncService) { final AsyncService asyncService,
final boolean includeCourses) {
super(asyncService); super(asyncService);
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.lmsSetup = lmsSetup; this.lmsSetup = lmsSetup;
this.moodleRestTemplateFactory = moodleRestTemplateFactory; this.moodleRestTemplateFactory = moodleRestTemplateFactory;
this.includeCourses = includeCourses;
} }
@Override @Override
@ -222,7 +226,7 @@ public class MoodleCourseAccess extends CourseAccess {
static Map<String, String> additionalAttrs = new HashMap<>(); static Map<String, String> additionalAttrs = new HashMap<>();
private static List<QuizData> quizDataOf( private List<QuizData> quizDataOf(
final LmsSetup lmsSetup, final LmsSetup lmsSetup,
final CourseData courseData, final CourseData courseData,
final String uriPrefix) { final String uriPrefix) {
@ -234,13 +238,13 @@ public class MoodleCourseAccess extends CourseAccess {
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_DISPLAY_NAME, courseData.display_name); additionalAttrs.put(QuizData.ATTR_ADDITIONAL_DISPLAY_NAME, courseData.display_name);
additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SUMMARY, courseData.summary); additionalAttrs.put(QuizData.ATTR_ADDITIONAL_SUMMARY, courseData.summary);
return courseData.quizzes final List<QuizData> courseAndQuiz = courseData.quizzes
.stream() .stream()
.map(courseQuizData -> { .map(courseQuizData -> {
final String startURI = uriPrefix + courseQuizData.id; 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(
courseQuizData.id, courseData.id + ":" + courseQuizData.id,
lmsSetup.getInstitutionId(), lmsSetup.getInstitutionId(),
lmsSetup.id, lmsSetup.id,
lmsSetup.getLmsType(), lmsSetup.getLmsType(),
@ -252,6 +256,22 @@ public class MoodleCourseAccess extends CourseAccess {
additionalAttrs); additionalAttrs);
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
if (this.includeCourses) {
courseAndQuiz.add(new QuizData(
courseData.id,
lmsSetup.getInstitutionId(),
lmsSetup.id,
lmsSetup.getLmsType(),
courseData.display_name,
courseData.full_name,
Utils.toDateTimeUTCUnix(courseData.start_date),
Utils.toDateTimeUTCUnix(courseData.end_date),
lmsSetup.lmsApiUrl + MOODLE_COURSE_START_URL_PATH + courseData.id,
additionalAttrs));
}
return courseAndQuiz;
} }
private Result<MoodleAPIRestTemplate> getRestTemplate() { private Result<MoodleAPIRestTemplate> getRestTemplate() {

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.ArrayList; import java.util.ArrayList;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
@ -27,11 +28,27 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestT
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction&courseId=123 * http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction&courseId=123
* *
* Response (JSON): * Response (JSON):
* {"courseId"="123", "configKeys"=["key1","key2","key3",...], "browserKeys"=["bkey1", "bkey2", "bkey3",...]} *
* <pre>
* {
* "courseId": "123",
* "quizId": "456",
* "configKeys": [
* "key1",
* "key2",
* "key3"
* ],
* "browserKeys": [
* "bkey1",
* "bkey2",
* "bkey3"
* ]
* }
* </pre>
* *
* Set keys: * Set keys:
* POST: * POST:
* http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction&courseId=123&configKey[0]=key1&configKey[1]=key2&browserKey[0]=bkey1&browserKey[1]=bkey2 * http://yourmoodle.org/webservice/rest/server.php?wstoken={token}&moodlewsrestformat=json&wsfunction=seb_restriction_update&courseId=123&configKey[0]=key1&configKey[1]=key2&browserKey[0]=bkey1&browserKey[1]=bkey2
* *
* Delete all key (and remove restrictions): * Delete all key (and remove restrictions):
* POST: * POST:
@ -41,8 +58,11 @@ public class MoodleCourseRestriction {
private static final Logger log = LoggerFactory.getLogger(MoodleCourseRestriction.class); private static final Logger log = LoggerFactory.getLogger(MoodleCourseRestriction.class);
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION = "seb_restriction"; private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION = "seb_restriction";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_CREATE = "seb_restriction_create";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_UPDATE = "seb_restriction_update";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE = "seb_restriction_delete"; private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE = "seb_restriction_delete";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_COURSE_ID = "courseId"; private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_COURSE_ID = "courseId";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID = "quizId";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_CONFIG_KEY = "configKey"; private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_CONFIG_KEY = "configKey";
private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY = "browserKey"; private static final String MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY = "browserKey";
@ -64,10 +84,29 @@ public class MoodleCourseRestriction {
return LmsSetupTestResult.ofQuizAccessAPIError("not available yet"); return LmsSetupTestResult.ofQuizAccessAPIError("not available yet");
} }
Result<MoodleSEBRestriction> getSEBRestriction(final String courseId) { Result<MoodleSEBRestriction> getSEBRestriction(
final String externalId) {
return Result.tryCatch(() -> {
final String[] courseQuizId = StringUtils.split(externalId, ":");
if (courseQuizId.length > 1) {
// we only have the course id (this is a course)
return getSEBRestriction(courseQuizId[0], null)
.getOrThrow();
} else {
// we have the course id and the quiz is (this is a quiz)
return getSEBRestriction(courseQuizId[0], courseQuizId[1])
.getOrThrow();
}
});
}
Result<MoodleSEBRestriction> getSEBRestriction(
final String courseId,
final String quizId) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("GET SEB Client restriction on course: {}", courseId); log.debug("GET SEB Client restriction on course: {} quiz: {}", courseId, quizId);
} }
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
@ -77,6 +116,9 @@ public class MoodleCourseRestriction {
final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_COURSE_ID, courseId); queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_COURSE_ID, courseId);
if (quizId != null) {
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID, quizId);
}
final String resultJSON = template.callMoodleAPIFunction( final String resultJSON = template.callMoodleAPIFunction(
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION, MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION,
@ -92,43 +134,103 @@ public class MoodleCourseRestriction {
}); });
} }
Result<Boolean> putSEBRestriction( Result<MoodleSEBRestriction> createSEBRestriction(
final String courseId, final String externalId,
final MoodleSEBRestriction restriction) { final MoodleSEBRestriction restriction) {
if (log.isDebugEnabled()) {
log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction);
}
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final String[] courseQuizId = StringUtils.split(externalId, ":");
final MoodleAPIRestTemplate template = getRestTemplate() if (courseQuizId.length > 1) {
.getOrThrow(); // we only have the course id (this is a course)
return createSEBRestriction(courseQuizId[0], null, restriction)
final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); .getOrThrow();
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_COURSE_ID, courseId); } else {
// we have the course id and the quiz is (this is a quiz)
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>(); return createSEBRestriction(courseQuizId[0], courseQuizId[1], restriction)
queryAttributes.addAll( .getOrThrow();
MOODLE_DEFAULT_COURSE_RESTRICTION_CONFIG_KEY, }
new ArrayList<>(restriction.configKeys));
queryAttributes.addAll(
MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY,
new ArrayList<>(restriction.browserExamKeys));
template.callMoodleAPIFunction(
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION,
queryParams,
queryAttributes);
return true;
}); });
} }
Result<Boolean> deleteSEBRestriction(final String courseId) { Result<MoodleSEBRestriction> createSEBRestriction(
final String courseId,
final String quizId,
final MoodleSEBRestriction restriction) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("DELETE SEB Client restriction on course: {}", courseId); log.debug("POST SEB Client restriction on course: {} quiz: restriction : {}",
courseId,
quizId,
restriction);
}
return postSEBRestriction(
courseId,
quizId,
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_CREATE,
restriction);
}
Result<MoodleSEBRestriction> updateSEBRestriction(
final String externalId,
final MoodleSEBRestriction restriction) {
return Result.tryCatch(() -> {
final String[] courseQuizId = StringUtils.split(externalId, ":");
if (courseQuizId.length > 1) {
// we only have the course id (this is a course)
return updateSEBRestriction(courseQuizId[0], null, restriction)
.getOrThrow();
} else {
// we have the course id and the quiz is (this is a quiz)
return updateSEBRestriction(courseQuizId[0], courseQuizId[1], restriction)
.getOrThrow();
}
});
}
Result<MoodleSEBRestriction> updateSEBRestriction(
final String courseId,
final String quizId,
final MoodleSEBRestriction restriction) {
if (log.isDebugEnabled()) {
log.debug("POST SEB Client restriction on course: {} quiz: restriction : {}",
courseId,
quizId,
restriction);
}
return postSEBRestriction(
courseId,
quizId,
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_UPDATE,
restriction);
}
Result<Boolean> deleteSEBRestriction(
final String externalId) {
return Result.tryCatch(() -> {
final String[] courseQuizId = StringUtils.split(externalId, ":");
if (courseQuizId.length > 1) {
// we only have the course id (this is a course)
return deleteSEBRestriction(courseQuizId[0], null)
.getOrThrow();
} else {
// we have the course id and the quiz is (this is a quiz)
return deleteSEBRestriction(courseQuizId[0], courseQuizId[1])
.getOrThrow();
}
});
}
Result<Boolean> deleteSEBRestriction(
final String courseId,
final String quizId) {
if (log.isDebugEnabled()) {
log.debug("DELETE SEB Client restriction on course: {} quizId {}", courseId, quizId);
} }
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
@ -137,6 +239,7 @@ public class MoodleCourseRestriction {
final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_COURSE_ID, courseId); queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_COURSE_ID, courseId);
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID, quizId);
template.callMoodleAPIFunction( template.callMoodleAPIFunction(
MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE, MOODLE_DEFAULT_COURSE_RESTRICTION_WS_FUNCTION_DELETE,
@ -161,4 +264,40 @@ public class MoodleCourseRestriction {
return Result.of(this.restTemplate); return Result.of(this.restTemplate);
} }
private Result<MoodleSEBRestriction> postSEBRestriction(
final String courseId,
final String quizId,
final String function,
final MoodleSEBRestriction restriction) {
return Result.tryCatch(() -> {
final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow();
final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_COURSE_ID, courseId);
queryParams.add(MOODLE_DEFAULT_COURSE_RESTRICTION_QUIZ_ID, quizId);
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
queryAttributes.addAll(
MOODLE_DEFAULT_COURSE_RESTRICTION_CONFIG_KEY,
new ArrayList<>(restriction.configKeys));
queryAttributes.addAll(
MOODLE_DEFAULT_COURSE_RESTRICTION_BROWSER_KEY,
new ArrayList<>(restriction.browserExamKeys));
final String resultJSON = template.callMoodleAPIFunction(
function,
queryParams,
queryAttributes);
final MoodleSEBRestriction restrictiondata = this.jsonMapper.readValue(
resultJSON,
new TypeReference<MoodleSEBRestriction>() {
});
return restrictiondata;
});
}
} }

View file

@ -115,7 +115,7 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
} }
return this.moodleCourseRestriction return this.moodleCourseRestriction
.putSEBRestriction( .updateSEBRestriction(
externalExamId, externalExamId,
MoodleSEBRestriction.from(sebRestrictionData)) MoodleSEBRestriction.from(sebRestrictionData))
.map(result -> sebRestrictionData); .map(result -> sebRestrictionData);
@ -127,7 +127,8 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
log.debug("Release SEB Client restriction for Exam: {}", exam); log.debug("Release SEB Client restriction for Exam: {}", exam);
} }
return this.moodleCourseRestriction.deleteSEBRestriction(exam.externalId) return this.moodleCourseRestriction
.deleteSEBRestriction(exam.externalId)
.map(result -> exam); .map(result -> exam);
} }

View file

@ -34,13 +34,15 @@ public class MoodleLmsAPITemplateFactory {
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final String[] alternativeTokenRequestPaths; private final String[] alternativeTokenRequestPaths;
private final boolean includeCourses;
protected MoodleLmsAPITemplateFactory( protected MoodleLmsAPITemplateFactory(
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final AsyncService asyncService, final AsyncService asyncService,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
@Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths) { @Value("${sebserver.webservice.lms.moodle.api.token.request.paths:}") final String alternativeTokenRequestPaths,
@Value("${sebserver.webservice.lms.moodle.api.includeCourses:false}") final boolean includeCourses) {
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.asyncService = asyncService; this.asyncService = asyncService;
@ -49,6 +51,7 @@ public class MoodleLmsAPITemplateFactory {
this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR) ? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR)
: null; : null;
this.includeCourses = includeCourses;
} }
public Result<MoodleLmsAPITemplate> create( public Result<MoodleLmsAPITemplate> create(
@ -71,7 +74,8 @@ public class MoodleLmsAPITemplateFactory {
this.jsonMapper, this.jsonMapper,
lmsSetup, lmsSetup,
moodleRestTemplateFactory, moodleRestTemplateFactory,
this.asyncService); this.asyncService,
this.includeCourses);
final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction( final MoodleCourseRestriction moodleCourseRestriction = new MoodleCourseRestriction(
this.jsonMapper, this.jsonMapper,

View file

@ -49,6 +49,7 @@ sebserver.webservice.api.pagination.maxPageSize=500
# comma separated list of known possible OpenEdX API access token request endpoints # comma separated list of known possible OpenEdX API access token request endpoints
sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token
sebserver.webservice.lms.moodle.api.token.request.paths= sebserver.webservice.lms.moodle.api.token.request.paths=
sebserver.webservice.lms.moodle.api.includeCourses=true
sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias
# NOTE: This is a temporary work-around for SEB Restriction API within Open edX SEB integration plugin to # NOTE: This is a temporary work-around for SEB Restriction API within Open edX SEB integration plugin to

View file

@ -51,4 +51,5 @@ sebserver.webservice.api.pagination.maxPageSize=500
# comma separated list of known possible OpenEdX API access token request endpoints # comma separated list of known possible OpenEdX API access token request endpoints
sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token
sebserver.webservice.lms.moodle.api.token.request.paths=/login/token.php sebserver.webservice.lms.moodle.api.token.request.paths=/login/token.php
sebserver.webservice.lms.moodle.api.includeCourses=false
sebserver.webservice.lms.address.alias= sebserver.webservice.lms.address.alias=