SEBSERV-301 SEBSERV-372

This commit is contained in:
anhefti 2022-12-22 17:10:41 +01:00
parent b9962a2609
commit 2434fce036
26 changed files with 548 additions and 369 deletions

View file

@ -63,6 +63,8 @@ public final class Exam implements GrantEntity {
public static final String ATTR_ADDITIONAL_ATTRIBUTES = "additionalAttributes";
/** This attribute name is used to store the number of quiz recover attempts done by exam update process */
public static final String ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS = "QUIZ_RECOVER_ATTEMPTS";
/** This attribute name is used on exams to store the flag for indicating the signature key check */
public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED = "SIGNATURE_KEY_CHECK_ENABLED";
/** This attribute name is used to store the signature check grant threshold for numerical trust checks */

View file

@ -19,6 +19,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MoodleSEBRestriction {
public static final String ATTR_ALT_BEK = "ALTERNATIVE_SEB_BEK";
public static final String ATTR_BROWSER_KEYS = "BROWSER_KEYS";
public static final String ATTR_CONFIG_KEYS = "CONFIG_KEYS";

View file

@ -240,7 +240,9 @@ public class ExamForm implements TemplateComposer {
.call()
.onError(e -> log.error("Unexpected error while trying to verify seb restriction settings: ", e))
.getOr(false);
final boolean sebRestrictionMismatch = isRestricted != exam.sebRestriction;
final boolean sebRestrictionMismatch = readonly &&
sebRestrictionAvailable &&
isRestricted != exam.sebRestriction;
// check exam consistency and inform the user if needed
Collection<APIMessage> warnings = null;
@ -669,14 +671,6 @@ public class ExamForm implements TemplateComposer {
}
}
private void showSEBRestrictionMismatchMessage(final Composite parent) {
final Composite warningPanel = this.widgetFactory.createWarningPanel(parent);
this.widgetFactory.labelLocalized(
warningPanel,
CustomVariant.MESSAGE,
CONSISTENCY_MESSAGE_SEB_RESTRICTION_MISMATCH);
}
private Result<Exam> getExistingExam(final PageContext pageContext) {
final EntityKey entityKey = pageContext.getEntityKey();
return this.restService.getBuilder(GetExam.class)

View file

@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
@ -73,6 +74,8 @@ public class ExamSEBRestrictionSettings {
new LocTextKey("sebserver.exam.form.sebrestriction.configKeys");
private final static LocTextKey SEB_RESTRICTION_FORM_BROWSER_KEYS =
new LocTextKey("sebserver.exam.form.sebrestriction.browserExamKeys");
private final static LocTextKey SEB_RESTRICTION_FORM_MOODLE_ALT_BEK_KEY =
new LocTextKey("sebserver.exam.form.sebrestriction.ALT_BEK_KEY");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_WHITE_LIST_PATHS =
new LocTextKey("sebserver.exam.form.sebrestriction.WHITELIST_PATHS");
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_PERMISSIONS =
@ -244,6 +247,16 @@ public class ExamSEBRestrictionSettings {
.asArea(50)
.readonly(true))
.addFieldIf(
() -> lmsType == LmsType.MOODLE_PLUGIN,
() -> FormBuilder.text(
MoodleSEBRestriction.ATTR_ALT_BEK,
SEB_RESTRICTION_FORM_MOODLE_ALT_BEK_KEY,
sebRestriction
.getAdditionalProperties()
.get(MoodleSEBRestriction.ATTR_ALT_BEK))
.readonly(true))
.addField(FormBuilder.text(
SEBRestriction.ATTR_BROWSER_KEYS,
SEB_RESTRICTION_FORM_BROWSER_KEYS,

View file

@ -44,12 +44,6 @@ public interface ExamAdminService {
* @return The exam with the initial additional attributes */
Result<Exam> initAdditionalAttributes(final Exam exam);
/** Saves additional attributes for the exam that are specific to a type of LMS
*
* @param exam The Exam to add the LMS specific attributes
* @return Result refer to the created exam or to an error when happened */
Result<Exam> saveLMSAttributes(Exam exam);
/** Saves the security key settings for an specific exam.
*
* @param institutionId The institution identifier

View file

@ -43,11 +43,11 @@ public interface ExamTemplateService {
* @return Result refer to the Exam with added client groups or to an error if happened */
Result<Exam> addDefinedClientGroups(Exam exam);
/** Initializes additional attributes for a specified Exam on creation.
/** Initializes additional exam template attributes for a specified Exam on creation.
*
* @param exam The Exam to add the default indicator
* @return Result refer to the created exam or to an error when happened */
Result<Exam> initAdditionalAttributes(Exam exam);
Result<Exam> initAdditionalTemplateAttributes(Exam exam);
/** Initializes a pre defined exam configuration. The configuration template to create a exam configuration
* is defined by a given linked exam template. This is used to create the exam configuration and automatically

View file

@ -118,7 +118,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
KeyGenerators.string().generateKey().toString());
return exam;
});
}).flatMap(this::initAdditionalAttributesForMoodleExams);
}
@Override
@ -186,11 +186,6 @@ public class ExamAdminServiceImpl implements ExamAdminService {
});
}
@Override
public Result<Exam> saveLMSAttributes(final Exam exam) {
return initAdditionalAttributesForMoodleExams(exam);
}
@Override
public Result<Boolean> isRestricted(final Exam exam) {
if (exam == null) {
@ -254,6 +249,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
.getLmsAPITemplate(exam.lmsSetupId)
.getOrThrow();
// TODO check if this is still needed
if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE) {
lmsTemplate.getQuiz(exam.externalId)
.flatMap(quizData -> this.additionalAttributesDAO.saveAdditionalAttribute(
@ -277,7 +273,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
EntityType.EXAM,
exam.id,
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
moodleBEK).getOrThrow();
moodleBEK)
.getOrThrow();
} catch (final Exception e) {
log.error("Failed to create additional moodle SEB BEK attribute: ", e);
}

View file

@ -57,7 +57,13 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
.getDefaultConfigurationNode(examId)
.flatMap(nodeId -> this.configurationDAO.getConfigurationLastStableVersion(nodeId))
.map(config -> config.id)
.getOrThrow();
.onError(error -> log.warn("Failed to get default Exam Config for exam: {} cause: {}",
examId, error.getMessage()))
.getOr(null);
if (configId == null) {
return null;
}
final Long attrId = this.configurationAttributeDAO
.getAttributeIdByName(configAttributeName)
@ -99,20 +105,22 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
log.error("Failed to get SEB restriction with quit secret: ", e);
}
return null;
return StringUtils.EMPTY;
}
@Override
public String getQuitLink(final Long examId) {
try {
return getMappedDefaultConfigAttributeValue(
final String quitLink = getMappedDefaultConfigAttributeValue(
examId,
CONFIG_ATTR_NAME_QUIT_LINK);
return (quitLink != null) ? quitLink : StringUtils.EMPTY;
} catch (final Exception e) {
log.error("Failed to get SEB restriction with quit link: ", e);
return null;
return StringUtils.EMPTY;
}
}

View file

@ -50,7 +50,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamTemplateDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
@Lazy
@ -61,7 +60,7 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
private static final Logger log = LoggerFactory.getLogger(ExamTemplateServiceImpl.class);
private final AdditionalAttributesDAO additionalAttributesDAO;
private final ExamAdminService examAdminService;
private final ExamTemplateDAO examTemplateDAO;
private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
@ -78,7 +77,6 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
public ExamTemplateServiceImpl(
final AdditionalAttributesDAO additionalAttributesDAO,
final ExamAdminService examAdminService,
final ExamTemplateDAO examTemplateDAO,
final ConfigurationNodeDAO configurationNodeDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
@ -97,7 +95,6 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
this.configurationNodeDAO = configurationNodeDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.additionalAttributesDAO = additionalAttributesDAO;
this.examAdminService = examAdminService;
this.indicatorDAO = indicatorDAO;
this.clientGroupDAO = clientGroupDAO;
this.jsonMapper = jsonMapper;
@ -155,41 +152,40 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
}
@Override
public Result<Exam> initAdditionalAttributes(final Exam exam) {
return this.examAdminService
.saveLMSAttributes(exam)
.map(_exam -> {
public Result<Exam> initAdditionalTemplateAttributes(final Exam exam) {
return Result.tryCatch(() -> {
if (exam.examTemplateId != null) {
if (exam.examTemplateId != null) {
if (log.isDebugEnabled()) {
log.debug("Init exam: {} with additional attributes from exam template: {}",
exam.externalId,
exam.examTemplateId);
}
if (log.isDebugEnabled()) {
log.debug("Init exam: {} with additional attributes from exam template: {}",
exam.externalId,
exam.examTemplateId);
}
final ExamTemplate examTemplate = this.examTemplateDAO
.byPK(exam.examTemplateId)
.onError(error -> log.warn("No exam template found for id: {}",
exam.examTemplateId,
error.getMessage()))
.getOr(null);
final ExamTemplate examTemplate = this.examTemplateDAO
.byPK(exam.examTemplateId)
.onError(error -> log.warn("No exam template found for id: {}",
exam.examTemplateId,
error.getMessage()))
.getOr(null);
if (examTemplate == null) {
return exam;
}
if (examTemplate == null) {
return exam;
}
if (examTemplate.examAttributes != null && !examTemplate.examAttributes.isEmpty()) {
this.additionalAttributesDAO.saveAdditionalAttributes(
EntityType.EXAM,
exam.getId(),
examTemplate.examAttributes);
}
}
return _exam;
}).onError(error -> log.error(
"Failed to create additional attributes defined by template for exam: ",
error));
if (examTemplate.examAttributes != null && !examTemplate.examAttributes.isEmpty()) {
this.additionalAttributesDAO.saveAdditionalAttributes(
EntityType.EXAM,
exam.getId(),
examTemplate.examAttributes);
}
}
return exam;
}).onError(error -> log.error(
"Failed to create additional attributes defined by template for exam: ",
error));
}
@Override

View file

@ -17,6 +17,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
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;
@ -81,6 +82,12 @@ public interface CourseAccessAPI {
* @return Result refer to the quiz data or to an error when happened */
Result<QuizData> getQuiz(final String id);
/** Tries to recover dangling exam that has lost its quiz data with the id mapping.
*
* @param exam The dangling exam to try to recover
* @return Result referring to the recovered QuizData or to an error when happened */
Result<QuizData> tryRecoverQuizForExam(Exam exam);
/** Clears the underling caches if there are some for a particular implementation. */
void clearCourseCache();

View file

@ -19,8 +19,7 @@ public interface SEBRestrictionService {
String SEB_RESTRICTION_ADDITIONAL_PROPERTY_CONFIG_KEY = "config_key";
/** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */
String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK =
SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX + "ALTERNATIVE_SEB_BEK";
String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK = "ALTERNATIVE_SEB_BEK";
/** Get the LmsAPIService that is used by the SEBRestrictionService */
LmsAPIService getLmsAPIService();

View file

@ -281,6 +281,26 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
.getOrThrow());
}
@Override
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
if (this.courseAccessAPI == null) {
return Result
.ofError(new UnsupportedOperationException("Course API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Try to recover quiz for exam {} for LMSSetup: {}", exam, lmsSetup());
}
return this.quizRequest.protectedRun(() -> this.courseAccessAPI
.tryRecoverQuizForExam(exam)
.onError(error -> log.error(
"Failed to run protectedQuizRecoverRequest: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public void clearCourseCache() {
if (this.courseAccessAPI != null) {
@ -372,8 +392,6 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
log.debug("Get course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup());
}
System.out.println("******************* getSEBClientRestriction");
return this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI
.getSEBClientRestriction(exam)
.onError(error -> log.error("Failed to get SEB restrictions: {}", error.getMessage()))

View file

@ -29,9 +29,11 @@ import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.MoodleSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
@ -135,6 +137,16 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
e);
}
// special Moodle plugin case for ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK
this.lmsAPIService.getLmsSetup(exam.lmsSetupId).map(lms -> {
if (lms.lmsType == LmsType.MOODLE_PLUGIN) {
additionalAttributes.put(
MoodleSEBRestriction.ATTR_ALT_BEK,
exam.getAdditionalAttribute(ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK));
}
return lms;
});
return new SEBRestriction(
exam.id,
configKeys,

View file

@ -216,6 +216,11 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
return quizRequest(id);
}
@Override
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
}
private List<QuizData> collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final List<QuizData> quizDatas = getAssignments(restTemplate)

View file

@ -45,6 +45,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
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.LmsSetup.LmsType;
@ -204,6 +205,11 @@ final class OpenEdxCourseAccess extends AbstractCachedCourseAccess implements Co
});
}
@Override
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
}
@Override
public Result<Collection<QuizData>> getQuizzes(final Set<String> ids) {
if (ids.size() == 1) {

View file

@ -24,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
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.LmsSetup.LmsType;
@ -233,6 +234,11 @@ public class MockCourseAccessAPI implements CourseAccessAPI {
.get());
}
@Override
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
}
@Override
public void clearCourseCache() {
// No cache here

View file

@ -9,7 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -18,6 +18,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@ -27,13 +28,16 @@ import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.core.JsonProcessingException;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleQuizRestriction;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseAccess;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MoodlePluginCourseRestriction;
public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
@ -144,18 +148,27 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
System.out.println("***** callMoodleAPIFunction HttpEntity: " + functionReqEntity);
// TODO return json
if (MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME.equals(functionName)) {
return respondCourses(queryAttributes);
} else if (MoodlePluginCourseAccess.QUIZZES_BY_COURSES_API_FUNCTION_NAME.equals(functionName)) {
return respondQuizzes(queryAttributes);
} else if (MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME.equals(functionName)) {
return respondUsers(queryAttributes);
} else {
} else if (MoodlePluginCourseRestriction.RESTRICTION_GET_FUNCTION_NAME.equals(functionName)) {
return respondGetRestriction(
queryParams.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIZ_ID),
queryAttributes);
} else if (MoodlePluginCourseRestriction.RESTRICTION_SET_FUNCTION_NAME.equals(functionName)) {
return respondSetRestriction(
queryParams.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIZ_ID),
queryAttributes);
}
else {
throw new RuntimeException("Unknown function: " + functionName);
}
}
@SuppressWarnings("unused")
private static final class MockCD {
public final String id;
public final String shortname;
@ -167,8 +180,9 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
public final Long enddate; // unix-time seconds UTC
public final Long timecreated; // unix-time seconds UTC
public final boolean visible;
public final Collection<MockQ> quizzes;
public MockCD(final String num) {
public MockCD(final String num, final Collection<MockQ> quizzes) {
this.id = num;
this.shortname = "c" + num;
this.categoryid = "mock";
@ -179,9 +193,11 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
this.enddate = null;
this.timecreated = Long.valueOf(num);
this.visible = true;
this.quizzes = quizzes;
}
}
@SuppressWarnings("unused")
private static final class MockQ {
public final String id;
public final String coursemodule;
@ -209,12 +225,17 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
System.out.println("************* from: " + from);
final List<MockCD> courses;
if (ids != null && !ids.isEmpty()) {
courses = ids.stream().map(id -> new MockCD(id)).collect(Collectors.toList());
courses = ids
.stream()
.map(id -> new MockCD(
id,
getQuizzesForCourse(Integer.parseInt(id))))
.collect(Collectors.toList());
} else if (from != null && Integer.valueOf(from) < 11) {
courses = new ArrayList<>();
final int num = (Integer.valueOf(from) > 0) ? 10 : 1;
for (int i = 0; i < 10; i++) {
courses.add(new MockCD(String.valueOf(num + i)));
courses.add(new MockCD(String.valueOf(num + i), getQuizzesForCourse(num + i)));
}
} else {
courses = new ArrayList<>();
@ -232,28 +253,56 @@ public class MockupRestTemplateFactory implements MoodleRestTemplateFactory {
}
}
private String respondQuizzes(final MultiValueMap<String, String> queryAttributes) {
try {
final List<String> ids = queryAttributes.get(MoodlePluginCourseAccess.CRITERIA_COURSE_IDS);
final List<MockQ> quizzes;
if (ids != null && !ids.isEmpty()) {
quizzes = ids.stream().map(id -> new MockQ(id, "10" + id)).collect(Collectors.toList());
} else {
quizzes = Collections.emptyList();
}
private final Map<String, MoodleQuizRestriction> restrcitions = new HashMap<>();
final Map<String, Object> response = new HashMap<>();
response.put("quizzes", quizzes);
final JSONMapper jsonMapper = new JSONMapper();
final String result = jsonMapper.writeValueAsString(response);
System.out.println("******** quizzes response: " + result);
return result;
private String respondSetRestriction(final String quizId, final MultiValueMap<String, String> queryAttributes) {
final List<String> configKeys = queryAttributes.get(MoodlePluginCourseRestriction.ATTRIBUTE_CONFIG_KEYS);
final List<String> beks = queryAttributes.get(MoodlePluginCourseRestriction.ATTRIBUTE_BROWSER_EXAM_KEYS);
final String quitURL = queryAttributes.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIT_URL);
final String quitSecret = queryAttributes.getFirst(MoodlePluginCourseRestriction.ATTRIBUTE_QUIT_SECRET);
final MoodleQuizRestriction moodleQuizRestriction = new MoodleQuizRestriction(
quizId,
StringUtils.join(configKeys, Constants.LIST_SEPARATOR),
StringUtils.join(beks, Constants.LIST_SEPARATOR),
quitURL,
quitSecret);
this.restrcitions.put(quizId, moodleQuizRestriction);
final JSONMapper jsonMapper = new JSONMapper();
try {
return jsonMapper.writeValueAsString(moodleQuizRestriction);
} catch (final JsonProcessingException e) {
e.printStackTrace();
return "";
}
}
private String respondGetRestriction(final String quizId, final MultiValueMap<String, String> queryAttributes) {
final MoodleQuizRestriction moodleQuizRestriction = this.restrcitions.get(quizId);
if (moodleQuizRestriction != null) {
final JSONMapper jsonMapper = new JSONMapper();
try {
return jsonMapper.writeValueAsString(moodleQuizRestriction);
} catch (final JsonProcessingException e) {
e.printStackTrace();
return "";
}
}
return "";
}
private Collection<MockQ> getQuizzesForCourse(final int courseId) {
final String id = String.valueOf(courseId);
final Collection<MockQ> result = new ArrayList<>();
result.add(new MockQ(id, "10" + id));
if (courseId % 2 > 0) {
result.add(new MockQ(id, "11" + id));
}
return result;
}
private String respondUsers(final MultiValueMap<String, String> queryAttributes) {
// TODO
return "";

View file

@ -47,7 +47,6 @@ public class MoodlePluginCheck {
try {
restTemplate.testAPIConnection(
MoodlePluginCourseAccess.COURSES_API_FUNCTION_NAME,
MoodlePluginCourseAccess.QUIZZES_BY_COURSES_API_FUNCTION_NAME,
MoodlePluginCourseAccess.USERS_API_FUNCTION_NAME);
} catch (final Exception e) {
log.info("Moodle SEB Server Plugin not available: {}", e.getMessage());

View file

@ -25,7 +25,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -249,22 +248,21 @@ public abstract class MoodleUtils {
public final Long end_date; // unix-time seconds UTC
public final Long time_created; // unix-time seconds UTC
public final String category_id;
@JsonIgnore
public final Collection<CourseQuiz> quizzes = new ArrayList<>();
public final Collection<CourseQuiz> quizzes;
@JsonCreator
public CourseData(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "shortname") final String short_name,
@JsonProperty(value = "idnumber") final String idnumber,
@JsonProperty(value = "fullname") final String full_name,
@JsonProperty(value = "displayname") final String display_name,
@JsonProperty(value = "summary") final String summary,
@JsonProperty(value = "startdate") final Long start_date,
@JsonProperty(value = "enddate") final Long end_date,
@JsonProperty(value = "timecreated") final Long time_created,
@JsonProperty(value = "categoryid") final String category_id) {
@JsonProperty("id") final String id,
@JsonProperty("shortname") final String short_name,
@JsonProperty("idnumber") final String idnumber,
@JsonProperty("fullname") final String full_name,
@JsonProperty("displayname") final String display_name,
@JsonProperty("summary") final String summary,
@JsonProperty("startdate") final Long start_date,
@JsonProperty("enddate") final Long end_date,
@JsonProperty("timecreated") final Long time_created,
@JsonProperty("categoryid") final String category_id,
@JsonProperty("quizzes") final Collection<CourseQuiz> quizzes) {
this.id = id;
this.short_name = short_name;
@ -276,6 +274,7 @@ public abstract class MoodleUtils {
this.end_date = end_date;
this.time_created = time_created;
this.category_id = category_id;
this.quizzes = (quizzes == null) ? new ArrayList<>() : quizzes;
}
}
@ -286,8 +285,8 @@ public abstract class MoodleUtils {
@JsonCreator
public Courses(
@JsonProperty(value = "courses") final Collection<CourseData> courses,
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
@JsonProperty("courses") final Collection<CourseData> courses,
@JsonProperty("warnings") final Collection<Warning> warnings) {
this.courses = courses;
this.warnings = warnings;
}
@ -300,8 +299,8 @@ public abstract class MoodleUtils {
@JsonCreator
public CourseQuizData(
@JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes,
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
@JsonProperty("quizzes") final Collection<CourseQuiz> quizzes,
@JsonProperty("warnings") final Collection<Warning> warnings) {
this.quizzes = quizzes;
this.warnings = warnings;
}
@ -320,14 +319,14 @@ public abstract class MoodleUtils {
@JsonCreator
public CourseQuiz(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "course") final String course,
@JsonProperty(value = "coursemodule") final String course_module,
@JsonProperty(value = "name") final String name,
@JsonProperty(value = "intro") final String intro,
@JsonProperty(value = "timeopen") final Long time_open,
@JsonProperty(value = "timeclose") final Long time_close,
@JsonProperty(value = "timelimit") final Long time_limit) {
@JsonProperty("id") final String id,
@JsonProperty("course") final String course,
@JsonProperty("coursemodule") final String course_module,
@JsonProperty("name") final String name,
@JsonProperty("intro") final String intro,
@JsonProperty("timeopen") final Long time_open,
@JsonProperty("timeclose") final Long time_close,
@JsonProperty("timelimit") final Long time_limit) {
this.id = id;
this.course = course;
@ -363,24 +362,24 @@ public abstract class MoodleUtils {
@JsonCreator
public MoodleUserDetails(
@JsonProperty(value = "id") final String id,
@JsonProperty(value = "username") final String username,
@JsonProperty(value = "firstname") final String firstname,
@JsonProperty(value = "lastname") final String lastname,
@JsonProperty(value = "fullname") final String fullname,
@JsonProperty(value = "email") final String email,
@JsonProperty(value = "department") final String department,
@JsonProperty(value = "firstaccess") final Long firstaccess,
@JsonProperty(value = "lastaccess") final Long lastaccess,
@JsonProperty(value = "auth") final String auth,
@JsonProperty(value = "suspended") final Boolean suspended,
@JsonProperty(value = "confirmed") final Boolean confirmed,
@JsonProperty(value = "lang") final String lang,
@JsonProperty(value = "theme") final String theme,
@JsonProperty(value = "timezone") final String timezone,
@JsonProperty(value = "description") final String description,
@JsonProperty(value = "mailformat") final Integer mailformat,
@JsonProperty(value = "descriptionformat") final Integer descriptionformat) {
@JsonProperty("id") final String id,
@JsonProperty("username") final String username,
@JsonProperty("firstname") final String firstname,
@JsonProperty("lastname") final String lastname,
@JsonProperty("fullname") final String fullname,
@JsonProperty("email") final String email,
@JsonProperty("department") final String department,
@JsonProperty("firstaccess") final Long firstaccess,
@JsonProperty("lastaccess") final Long lastaccess,
@JsonProperty("auth") final String auth,
@JsonProperty("suspended") final Boolean suspended,
@JsonProperty("confirmed") final Boolean confirmed,
@JsonProperty("lang") final String lang,
@JsonProperty("theme") final String theme,
@JsonProperty("timezone") final String timezone,
@JsonProperty("description") final String description,
@JsonProperty("mailformat") final Integer mailformat,
@JsonProperty("descriptionformat") final Integer descriptionformat) {
this.id = id;
this.username = username;
@ -403,52 +402,15 @@ public abstract class MoodleUtils {
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static final class MoodlePluginUserDetails {
public final String id;
public final String fullname;
public final String username;
public final String firstname;
public final String lastname;
public final String idnumber;
public final String email;
public final Map<String, String> customfields;
@JsonCreator
public MoodlePluginUserDetails(
final String id,
final String username,
final String firstname,
final String lastname,
final String idnumber,
final String email,
final Map<String, String> customfields) {
this.id = id;
if (firstname != null && lastname != null) {
this.fullname = firstname + Constants.SPACE + lastname;
} else if (firstname != null) {
this.fullname = firstname;
} else {
this.fullname = lastname;
}
this.username = username;
this.firstname = firstname;
this.lastname = lastname;
this.idnumber = idnumber;
this.email = email;
this.customfields = Utils.immutableMapOf(customfields);
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static final class CoursePage {
public final Collection<CourseKey> courseKeys;
public final Collection<Warning> warnings;
@JsonCreator
public CoursePage(
@JsonProperty(value = "courses") final Collection<CourseKey> courseKeys,
@JsonProperty(value = "warnings") final Collection<Warning> warnings) {
@JsonProperty("courses") final Collection<CourseKey> courseKeys,
@JsonProperty("warnings") final Collection<Warning> warnings) {
this.courseKeys = courseKeys;
this.warnings = warnings;
@ -464,10 +426,10 @@ public abstract class MoodleUtils {
@JsonCreator
public 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) {
@JsonProperty("id") final String id,
@JsonProperty("shortname") final String short_name,
@JsonProperty("categoryname") final String category_name,
@JsonProperty("sortorder") final String sort_order) {
this.id = id;
this.short_name = short_name;
@ -501,11 +463,11 @@ public abstract class MoodleUtils {
@JsonCreator
public MoodleQuizRestriction(
final String quiz_id,
final String config_keys,
final String browser_exam_keys,
final String quit_link,
final String quit_secret) {
@JsonProperty("quiz_id") final String quiz_id,
@JsonProperty("config_keys") final String config_keys,
@JsonProperty("browser_exam_keys") final String browser_exam_keys,
@JsonProperty("quit_link") final String quit_link,
@JsonProperty("quit_secret") final String quit_secret) {
this.quiz_id = quiz_id;
this.config_keys = config_keys;

View file

@ -38,6 +38,7 @@ import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
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.LmsSetup.LmsType;
@ -87,6 +88,7 @@ public class MoodleCourseAccess implements CourseAccessAPI {
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_API_COURSE_SHORTNAME = "shortname";
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";
@ -332,6 +334,71 @@ public class MoodleCourseAccess implements CourseAccessAPI {
});
}
@Override
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = getApiTemplateDataSupplier().getLmsSetup();
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
// get the course name identifier for recovering
final String shortname = MoodleUtils.getShortname(exam.externalId);
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
attributes.add(MOODLE_COURSE_API_FIELD_NAME, MOODLE_COURSE_API_COURSE_SHORTNAME);
attributes.add(MOODLE_COURSE_API_FIELD_VALUE, shortname);
final String coursePageJSON = restTemplate.callMoodleAPIFunction(
MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME,
attributes);
final Map<String, CourseData> courses = this.jsonMapper.readValue(
coursePageJSON,
Courses.class).courses
.stream()
.collect(Collectors.toMap(c -> c.id, Function.identity()));
// then get all quizzes of courses and filter
final LinkedMultiValueMap<String, String> cAttributes = new LinkedMultiValueMap<>();
final List<String> courseIds = new ArrayList<>(courses.keySet());
if (courseIds.size() == 1) {
// NOTE: This is a workaround because the Moodle API do not support lists with only one element.
courseIds.add("0");
}
cAttributes.put(
MoodleCourseAccess.MOODLE_COURSE_API_COURSE_IDS,
courseIds);
final String quizzesJSON = this.protectedMoodlePageCall
.protectedRun(() -> restTemplate.callMoodleAPIFunction(
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
attributes))
.getOrThrow();
this.jsonMapper.readValue(
quizzesJSON,
CourseQuizData.class).quizzes.stream().forEach(quiz -> {
final CourseData data = courses.get(quiz.course);
if (data != null) {
data.quizzes.add(quiz);
}
});
return courses.values()
.stream()
.flatMap(c -> MoodleUtils.quizDataOf(
lmsSetup,
c,
urlPrefix,
this.prependShortCourseName).stream())
.filter(q -> exam.name.contains(q.name))
.findFirst()
.get();
});
}
@Override
public void clearCourseCache() {

View file

@ -19,7 +19,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -43,6 +42,7 @@ import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
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.LmsSetup.LmsType;
@ -58,9 +58,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRe
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.CourseQuizData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.Courses;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodlePluginUserDetails;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleUserDetails;
public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess implements CourseAccessAPI {
@ -68,10 +67,12 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
public static final String MOODLE_QUIZ_START_URL_PATH = "mod/quiz/view.php?id=";
public static final String COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_courses";
public static final String QUIZZES_BY_COURSES_API_FUNCTION_NAME = "quizaccess_sebserver_get_quizzes_by_courses";
public static final String USERS_API_FUNCTION_NAME = "quizaccess_sebserver_get_users";
public static final String USERS_API_FUNCTION_NAME = "core_user_get_users_by_field";
public static final String ATTR_FIELD = "field";
public static final String ATTR_VALUE = "value";
public static final String ATTR_ID = "id";
public static final String ATTR_SHORTNAME = "shortname";
public static final String CRITERIA_COURSE_IDS = "ids";
public static final String CRITERIA_FROM_DATE = "from_date";
public static final String CRITERIA_TO_DATE = "to_date";
@ -147,7 +148,6 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
restTemplate.testAPIConnection(
COURSES_API_FUNCTION_NAME,
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
USERS_API_FUNCTION_NAME);
} catch (final RuntimeException e) {
@ -243,15 +243,50 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
return Result.tryCatch(() -> {
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
final MoodleAPIRestTemplate restTemplate = getRestTemplate().getOrThrow();
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
// get the course name identifier for recovering
final String shortname = MoodleUtils.getShortname(exam.externalId);
final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
attributes.add(ATTR_FIELD, ATTR_SHORTNAME);
attributes.add(ATTR_VALUE, shortname);
final String courseJSON = restTemplate.callMoodleAPIFunction(
COURSES_API_FUNCTION_NAME,
attributes);
return this.jsonMapper.readValue(
courseJSON,
Courses.class).courses
.stream()
.flatMap(c -> MoodleUtils.quizDataOf(
lmsSetup,
c,
urlPrefix,
this.prependShortCourseName).stream())
.filter(q -> exam.name.contains(q.name))
.findFirst()
.get();
});
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeSessionId) {
return Result.tryCatch(() -> {
final MoodleAPIRestTemplate template = getRestTemplate()
.getOrThrow();
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
queryAttributes.add(ATTR_FIELD, "id");
queryAttributes.add("values[0]", examineeUserId);
queryAttributes.add(ATTR_FIELD, ATTR_ID);
queryAttributes.add(ATTR_VALUE, examineeSessionId);
final String userDetailsJSON = template.callMoodleAPIFunction(
USERS_API_FUNCTION_NAME,
@ -266,21 +301,36 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
throw new RuntimeException("No user details on Moodle API request (access-denied)");
}
final MoodlePluginUserDetails[] userDetails = this.jsonMapper.<MoodlePluginUserDetails[]> readValue(
final MoodleUserDetails[] userDetails = this.jsonMapper.<MoodleUserDetails[]> readValue(
userDetailsJSON,
new TypeReference<MoodlePluginUserDetails[]>() {
new TypeReference<MoodleUserDetails[]>() {
});
if (userDetails == null || userDetails.length <= 0) {
throw new RuntimeException("No user details on Moodle API request");
}
final Map<String, String> additionalAttributes = new HashMap<>();
additionalAttributes.put("firstname", userDetails[0].firstname);
additionalAttributes.put("lastname", userDetails[0].lastname);
additionalAttributes.put("department", userDetails[0].department);
additionalAttributes.put("firstaccess", String.valueOf(userDetails[0].firstaccess));
additionalAttributes.put("lastaccess", String.valueOf(userDetails[0].lastaccess));
additionalAttributes.put("auth", userDetails[0].auth);
additionalAttributes.put("suspended", String.valueOf(userDetails[0].suspended));
additionalAttributes.put("confirmed", String.valueOf(userDetails[0].confirmed));
additionalAttributes.put("lang", userDetails[0].lang);
additionalAttributes.put("theme", userDetails[0].theme);
additionalAttributes.put("timezone", userDetails[0].timezone);
additionalAttributes.put("description", userDetails[0].description);
additionalAttributes.put("mailformat", String.valueOf(userDetails[0].mailformat));
additionalAttributes.put("descriptionformat", String.valueOf(userDetails[0].descriptionformat));
return new ExamineeAccountDetails(
userDetails[0].id,
userDetails[0].fullname,
userDetails[0].username,
userDetails[0].email,
userDetails[0].customfields);
additionalAttributes);
});
}
@ -314,63 +364,16 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
// first get courses page from moodle
final Map<String, CourseData> courseData = new HashMap<>();
final Collection<CourseData> coursesPage = getCoursesPage(restTemplate, quizFromTime, page, this.pageSize);
// no courses for page --> finish
if (coursesPage == null || coursesPage.isEmpty()) {
final Collection<CourseData> fetchCoursesPage =
fetchCoursesPage(restTemplate, quizFromTime, page, this.pageSize);
// finish if page is empty (no courses left
if (fetchCoursesPage.isEmpty()) {
asyncQuizFetchBuffer.finish();
return;
}
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<>();
final List<String> courseIds = new ArrayList<>(courseData.keySet());
attributes.put(CRITERIA_COURSE_IDS, courseIds);
final String quizzesJSON = this.protectedMoodlePageCall
.protectedRun(() -> restTemplate.callMoodleAPIFunction(
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
attributes))
.getOrThrow();
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
quizzesJSON,
CourseQuizData.class);
if (courseQuizData == null) {
return; // SEBSERV-361
}
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
MoodleUtils.logMoodleWarning(
courseQuizData.warnings,
lmsSetup.name,
QUIZZES_BY_COURSES_API_FUNCTION_NAME);
}
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
return; // no quizzes on this page
}
courseQuizData.quizzes
.stream()
.filter(MoodleUtils.getQuizFilter())
.forEach(quiz -> {
final CourseData data = courseData.get(quiz.course);
if (data != null) {
data.quizzes.add(quiz);
}
});
courseData.values().stream()
// fetch and buffer quizzes
fetchCoursesPage.stream()
.filter(c -> !c.quizzes.isEmpty())
.forEach(c -> asyncQuizFetchBuffer.buffer.addAll(
MoodleUtils.quizDataOf(lmsSetup, c, urlPrefix, this.prependShortCourseName)
@ -378,18 +381,23 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
.filter(quizFilter)
.collect(Collectors.toList())));
// check thresholds
if (asyncQuizFetchBuffer.buffer.size() > this.maxSize) {
log.warn("Maximal moodle quiz fetch size of {} reached. Cancel fetch at this point.", this.maxSize);
asyncQuizFetchBuffer.finish();
}
}
private Collection<CourseData> getCoursesPage(
private Collection<CourseData> fetchCoursesPage(
final MoodleAPIRestTemplate restTemplate,
final DateTime quizFromTime,
final int page,
final int size) throws JsonParseException, JsonMappingException, IOException {
if (log.isDebugEnabled()) {
log.debug("Fetch course page: {}, size: {} quizFromTime: {}", page, size, quizFromTime);
}
final String lmsName = getLmsSetupName();
try {
// get course ids per page
@ -442,59 +450,31 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
private List<QuizData> getQuizzesForIds(
final MoodleAPIRestTemplate restTemplate,
final Set<String> quizIds) {
final Set<String> internalIds) {
try {
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
if (log.isDebugEnabled()) {
log.debug("Get quizzes for ids: {} and LMSSetup: {}", quizIds, lmsSetup);
}
// get involved courses and map by course id
final Map<String, CourseData> courseData = getCoursesForIds(
restTemplate,
quizIds.stream()
.map(MoodleUtils::getCourseId)
.collect(Collectors.toSet()))
.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(CRITERIA_COURSE_IDS, new ArrayList<>(courseData.keySet()));
final String quizzesJSON = restTemplate.callMoodleAPIFunction(
QUIZZES_BY_COURSES_API_FUNCTION_NAME,
attributes);
final CourseQuizData courseQuizData = this.jsonMapper.readValue(
quizzesJSON,
CourseQuizData.class);
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
MoodleUtils.logMoodleWarning(
courseQuizData.warnings,
lmsSetup.name,
QUIZZES_BY_COURSES_API_FUNCTION_NAME);
}
final Map<String, CourseData> finalCourseDataRef = courseData;
courseQuizData.quizzes
.stream()
.forEach(quiz -> MoodleUtils.fillSelectedQuizzes(quizIds, finalCourseDataRef, quiz));
final String urlPrefix = (lmsSetup.lmsApiUrl.endsWith(Constants.URL_PATH_SEPARATOR))
? lmsSetup.lmsApiUrl + MOODLE_QUIZ_START_URL_PATH
: lmsSetup.lmsApiUrl + Constants.URL_PATH_SEPARATOR + MOODLE_QUIZ_START_URL_PATH;
final Set<String> moodleCourseIds = internalIds.stream()
.map(MoodleUtils::getCourseId)
.collect(Collectors.toSet());
return courseData.values()
if (log.isDebugEnabled()) {
log.debug("Get quizzes for internal ids: {}, Moodle courseI ids: {} and LMSSetup: {}",
internalIds,
moodleCourseIds,
lmsSetup);
}
return getCoursesForIds(restTemplate, moodleCourseIds)
.stream()
.filter(c -> !c.quizzes.isEmpty())
.flatMap(cd -> MoodleUtils.quizDataOf(
.filter(courseData -> !courseData.quizzes.isEmpty())
.flatMap(courseData -> MoodleUtils.quizDataOf(
lmsSetup,
cd,
courseData,
urlPrefix,
this.prependShortCourseName).stream())
.collect(Collectors.toList());

View file

@ -105,7 +105,10 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final LinkedMultiValueMap<String, String> addQuery = new LinkedMultiValueMap<>();
addQuery.add(ATTRIBUTE_QUIZ_ID, quizId);
final String srJSON = restTemplate.callMoodleAPIFunction(RESTRICTION_GET_FUNCTION_NAME, addQuery);
final String srJSON = restTemplate.callMoodleAPIFunction(
RESTRICTION_GET_FUNCTION_NAME,
addQuery,
new LinkedMultiValueMap<>());
try {
@ -139,9 +142,10 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final ArrayList<String> configKeys = new ArrayList<>(sebRestrictionData.configKeys);
final String quitLink = this.examConfigurationValueService.getQuitLink(exam.id);
final String quitSecret = this.examConfigurationValueService.getQuitSecret(exam.id);
final String additionalBEK = sebRestrictionData.additionalProperties.get(
final String additionalBEK = exam.getAdditionalAttribute(
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK);
if (additionalBEK != null) {
if (additionalBEK != null && !beks.contains(additionalBEK)) {
beks.add(additionalBEK);
}
@ -200,18 +204,12 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final List<String> configKeys = Arrays.asList(StringUtils.split(
moodleRestriction.config_keys,
Constants.LIST_SEPARATOR));
final List<String> browserExamKeys = Arrays.asList(StringUtils.split(
final List<String> browserExamKeys = new ArrayList<>(Arrays.asList(StringUtils.split(
moodleRestriction.browser_exam_keys,
Constants.LIST_SEPARATOR));
Constants.LIST_SEPARATOR)));
final Map<String, String> additionalProperties = new HashMap<>();
additionalProperties.put(ATTRIBUTE_QUIT_URL, moodleRestriction.quit_link);
final String additionalBEK = exam.getAdditionalAttribute(
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK);
if (additionalBEK != null) {
browserExamKeys.remove(additionalBEK);
}
additionalProperties.put(ATTRIBUTE_QUIT_SECRET, moodleRestriction.quit_secret);
return new SEBRestriction(
exam.id,

View file

@ -220,6 +220,11 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
return quizRequest(id);
}
@Override
public Result<QuizData> tryRecoverQuizForExam(final Exam exam) {
return Result.ofError(new UnsupportedOperationException("Recovering not supported"));
}
@Override
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId));

View file

@ -14,7 +14,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
@ -24,21 +23,19 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
@ -51,23 +48,28 @@ class ExamUpdateHandler {
private static final Logger log = LoggerFactory.getLogger(ExamUpdateHandler.class);
private final ExamDAO examDAO;
private final AdditionalAttributesDAO additionalAttributesDAO;
private final ApplicationEventPublisher applicationEventPublisher;
private final SEBRestrictionService sebRestrictionService;
private final LmsAPIService lmsAPIService;
private final String updatePrefix;
private final Long examTimeSuffix;
private final boolean tryRecoverExam;
private final int recoverAttempts;
public ExamUpdateHandler(
final ExamDAO examDAO,
final AdditionalAttributesDAO additionalAttributesDAO,
final ApplicationEventPublisher applicationEventPublisher,
final SEBRestrictionService sebRestrictionService,
final LmsAPIService lmsAPIService,
final WebserviceInfo webserviceInfo,
@Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix,
@Value("${sebserver.webservice.api.exam.tryrecover:false}") final boolean tryRecoverExam) {
@Value("${sebserver.webservice.api.exam.tryrecover:true}") final boolean tryRecoverExam,
@Value("${sebserver.webservice.api.exam.recoverattempts:3}") final int recoverAttempts) {
this.examDAO = examDAO;
this.additionalAttributesDAO = additionalAttributesDAO;
this.applicationEventPublisher = applicationEventPublisher;
this.sebRestrictionService = sebRestrictionService;
this.lmsAPIService = lmsAPIService;
@ -75,6 +77,7 @@ class ExamUpdateHandler {
+ "_" + webserviceInfo.getServerPort() + "_";
this.examTimeSuffix = examTimeSuffix;
this.tryRecoverExam = tryRecoverExam;
this.recoverAttempts = recoverAttempts;
}
public SEBRestrictionService getSEBRestrictionService() {
@ -379,67 +382,124 @@ class ExamUpdateHandler {
final String updateId) {
return Result.tryCatch(() -> {
final Exam exam = exams.get(quizId);
final LmsAPITemplate lmsTemplate = this.lmsAPIService
.getLmsAPITemplate(lmsSetupId)
.getOrThrow();
final int attempts = Integer.parseInt(this.additionalAttributesDAO.getAdditionalAttribute(
EntityType.EXAM,
exam.id,
Exam.ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS)
.map(AdditionalAttributeRecord::getValue)
.getOr("0"));
// If this is a Moodle quiz, try to recover from eventually restore of the quiz on the LMS side
// NOTE: This is a workaround for Moodle quizzes that had have a recovery within the sandbox tool
// Where potentially quiz identifiers get changed during such a recovery and the SEB Server
// internal mapping is not working properly anymore. In this case we try to recover from such
// a case by using the short name of the quiz and search for the quiz within the course with this
// short name. If one quiz has been found that matches all criteria, we adapt the internal id
// mapping to this quiz.
// If recovering fails, this returns null and the calling side must handle the lack of quiz data
if (lmsTemplate.getType() == LmsType.MOODLE) {
log.info("Try to recover quiz data for Moodle quiz with internal identifier: {}", quizId);
if (exam != null && exam.name != null
&& !exam.name.startsWith(Constants.SQUARE_BRACE_OPEN.toString())) {
log.debug("Found formerName quiz name: {}", exam.name);
// get the course name identifier
final String shortname = MoodleUtils.getShortname(quizId);
if (StringUtils.isNotBlank(shortname)) {
log.debug("Using short-name: {} for recovering", shortname);
final QuizData recoveredQuizData = lmsTemplate
.getQuizzes(new FilterMap())
.getOrThrow()
.stream()
.filter(quiz -> {
final String qShortName = MoodleUtils.getShortname(quiz.id);
return qShortName != null && qShortName.equals(shortname);
})
.filter(quiz -> exam.name.equals(quiz.name))
.findAny()
.get();
if (recoveredQuizData != null) {
log.debug("Found quiz data for recovering: {}", recoveredQuizData);
// save exam with new external id and quit data
this.examDAO
.updateQuizData(exam.id, recoveredQuizData, updateId)
.getOrThrow();
log.debug("Successfully recovered exam quiz data to new externalId {}",
recoveredQuizData.id);
}
return recoveredQuizData;
}
if (attempts >= this.recoverAttempts) {
if (log.isDebugEnabled()) {
log.debug("Skip recovering quiz due to too many attempts: {}", exam.getModelId());
throw new RuntimeException("Recover attempts reached");
}
}
if (exam.lmsAvailable == null || exam.isLmsAvailable()) {
this.examDAO.markLMSAvailability(quizId, false, updateId);
}
throw new RuntimeException("Not Available");
log.info(
"Try to recover quiz data for Moodle quiz with internal identifier: {}",
quizId);
return this.lmsAPIService
.getLmsAPITemplate(lmsSetupId)
.flatMap(template -> template.tryRecoverQuizForExam(exam))
.onError(error -> {
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
exam.id,
Exam.ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS,
String.valueOf(attempts + 1))
.onError(error1 -> log.error("Failed to save new attempts: ", error1));
if (exam.lmsAvailable == null || exam.isLmsAvailable()) {
this.examDAO.markLMSAvailability(quizId, false, updateId);
}
})
.getOrThrowRuntime("Not Available");
// // If this is a Moodle quiz, try to recover from eventually restore of the quiz on the LMS side
// // NOTE: This is a workaround for Moodle quizzes that had have a recovery within the sandbox tool
// // Where potentially quiz identifiers get changed during such a recovery and the SEB Server
// // internal mapping is not working properly anymore. In this case we try to recover from such
// // a case by using the short name of the quiz and search for the quiz within the course with this
// // short name. If one quiz has been found that matches all criteria, we adapt the internal id
// // mapping to this quiz.
// // If recovering fails, this returns null and the calling side must handle the lack of quiz data
// if (lmsTemplate.getType() == LmsType.MOODLE || lmsTemplate.getType() == LmsType.MOODLE_PLUGIN) {
//
// final int attempts = Integer.parseInt(this.additionalAttributesDAO.getAdditionalAttribute(
// EntityType.EXAM,
// exam.id,
// Exam.ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS)
// .map(AdditionalAttributeRecord::getValue)
// .getOr("0"));
//
// if (attempts >= this.recoverAttempts) {
// if (log.isDebugEnabled()) {
// log.debug("Skip recovering quiz due to too many attempts: {}", exam.getModelId());
// throw new RuntimeException("Recover attempts reached");
// }
// }
//
// log.info(
// "Try to recover quiz data for Moodle quiz with internal identifier: {}",
// quizId);
//
// if (exam != null && exam.name != null
// && !exam.name.startsWith(Constants.SQUARE_BRACE_OPEN.toString())) {
//
// log.debug("Found formerName quiz name: {}", exam.name);
//
// // get the course name identifier
// final String shortname = MoodleUtils.getShortname(quizId);
// if (StringUtils.isNotBlank(shortname)) {
//
// log.debug("Using short-name: {} for recovering", shortname);
//
// final QuizData recoveredQuizData = lmsTemplate
// .getQuizzes(new FilterMap())
// .getOrThrow()
// .stream()
// .filter(quiz -> {
// final String qShortName = MoodleUtils.getShortname(quiz.id);
// return qShortName != null && qShortName.equals(shortname);
// })
// .filter(quiz -> exam.name.equals(quiz.name))
// .findAny()
// .get();
//
// if (recoveredQuizData != null) {
//
// log.debug("Found quiz data for recovering: {}", recoveredQuizData);
//
// // save exam with new external id and quit data
// this.examDAO
// .updateQuizData(exam.id, recoveredQuizData, updateId)
// .getOrThrow();
//
// log.debug("Successfully recovered exam quiz data to new externalId {}",
// recoveredQuizData.id);
// }
// return recoveredQuizData;
// }
// }
//
// this.additionalAttributesDAO.saveAdditionalAttribute(
// EntityType.EXAM,
// exam.id,
// Exam.ADDITIONAL_ATTR_QUIZ_RECOVER_ATTEMPTS,
// String.valueOf(attempts + 1))
// .onError(error -> log.error("Failed to save new attempts: ", error));
// }
//
// if (exam.lmsAvailable == null || exam.isLmsAvailable()) {
// this.examDAO.markLMSAvailability(quizId, false, updateId);
// }
//
// throw new RuntimeException("Not Available");
});
}

View file

@ -528,7 +528,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CLIENT_GROUPS.of(error));
return entity;
})
.flatMap(this.examTemplateService::initAdditionalAttributes)
.flatMap(this.examTemplateService::initAdditionalTemplateAttributes)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error));
return entity;
@ -563,7 +563,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return Result.tryCatch(() -> {
this.examSessionService.flushCache(entity);
return entity;
}).flatMap(this.examAdminService::saveLMSAttributes);
});
}
@Override

View file

@ -586,6 +586,7 @@ sebserver.exam.form.sebrestriction.PERMISSION_COMPONENTS=Permissions
sebserver.exam.form.sebrestriction.PERMISSION_COMPONENTS.tooltip=Define the additional SEB restriction permissions
sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED=User Banning
sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED.tooltip=Indicates whether the user of a restricted access shall be banned on authentication failure or not
sebserver.exam.form.sebrestriction.ALT_BEK_KEY=SEB Server Browser Exam Key
sebserver.exam.form.sebrestriction.whiteListPaths.ABOUT=About
sebserver.exam.form.sebrestriction.whiteListPaths.ABOUT.tooltip=The "About" section of the Open edX course