diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 02fe543a..81fa9d0e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -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 */ diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/MoodleSEBRestriction.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/MoodleSEBRestriction.java index eb9210cb..34388ae0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/MoodleSEBRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/MoodleSEBRestriction.java @@ -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"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java index d92e5c21..3e30ea9e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java @@ -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 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 getExistingExam(final PageContext pageContext) { final EntityKey entityKey = pageContext.getEntityKey(); return this.restService.getBuilder(GetExam.class) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSEBRestrictionSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSEBRestrictionSettings.java index 70cd080a..7b66f024 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSEBRestrictionSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSEBRestrictionSettings.java @@ -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, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index adf23018..05819029 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -44,12 +44,6 @@ public interface ExamAdminService { * @return The exam with the initial additional attributes */ Result 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 saveLMSAttributes(Exam exam); - /** Saves the security key settings for an specific exam. * * @param institutionId The institution identifier diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java index ed65bfdb..27174427 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java @@ -43,11 +43,11 @@ public interface ExamTemplateService { * @return Result refer to the Exam with added client groups or to an error if happened */ Result 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 initAdditionalAttributes(Exam exam); + Result 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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index 888a382d..52f32452 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -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 saveLMSAttributes(final Exam exam) { - return initAdditionalAttributesForMoodleExams(exam); - } - @Override public Result 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); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamConfigurationValueServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamConfigurationValueServiceImpl.java index 6973a51c..ebc0b085 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamConfigurationValueServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamConfigurationValueServiceImpl.java @@ -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; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java index 76d3dc78..d46fa2bf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java @@ -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 initAdditionalAttributes(final Exam exam) { - return this.examAdminService - .saveLMSAttributes(exam) - .map(_exam -> { + public Result 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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java index 0d271870..7f4ccaf9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java @@ -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 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 tryRecoverQuizForExam(Exam exam); + /** Clears the underling caches if there are some for a particular implementation. */ void clearCourseCache(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java index 1ce96930..d13a21ba 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java @@ -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(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java index 34e82cd6..f1fb726f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java @@ -281,6 +281,26 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { .getOrThrow()); } + @Override + public Result 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())) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java index f629d9e1..adc54dd2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java @@ -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, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java index 2fc3ec6a..fc74b1f6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java @@ -216,6 +216,11 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms return quizRequest(id); } + @Override + public Result tryRecoverQuizForExam(final Exam exam) { + return Result.ofError(new UnsupportedOperationException("Recovering not supported")); + } + private List collectAllQuizzes(final AnsPersonalRestTemplate restTemplate) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final List quizDatas = getAssignments(restTemplate) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java index a6ed36a9..f530d2a7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java @@ -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 tryRecoverQuizForExam(final Exam exam) { + return Result.ofError(new UnsupportedOperationException("Recovering not supported")); + } + @Override public Result> getQuizzes(final Set ids) { if (ids.size() == 1) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java index 7becaef1..c8bf545a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java @@ -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 tryRecoverQuizForExam(final Exam exam) { + return Result.ofError(new UnsupportedOperationException("Recovering not supported")); + } + @Override public void clearCourseCache() { // No cache here diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MockupRestTemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MockupRestTemplateFactory.java index 4d88fb70..60b49d3b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MockupRestTemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MockupRestTemplateFactory.java @@ -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 quizzes; - public MockCD(final String num) { + public MockCD(final String num, final Collection 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 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 queryAttributes) { - try { - final List ids = queryAttributes.get(MoodlePluginCourseAccess.CRITERIA_COURSE_IDS); - final List 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 restrcitions = new HashMap<>(); - final Map 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 queryAttributes) { + final List configKeys = queryAttributes.get(MoodlePluginCourseRestriction.ATTRIBUTE_CONFIG_KEYS); + final List 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 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 getQuizzesForCourse(final int courseId) { + final String id = String.valueOf(courseId); + final Collection 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 queryAttributes) { // TODO return ""; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodlePluginCheck.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodlePluginCheck.java index 0bd91a7d..fd720eaf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodlePluginCheck.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodlePluginCheck.java @@ -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()); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleUtils.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleUtils.java index 85f79fe9..3540d1c7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleUtils.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleUtils.java @@ -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 quizzes = new ArrayList<>(); + public final Collection 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 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 courses, - @JsonProperty(value = "warnings") final Collection warnings) { + @JsonProperty("courses") final Collection courses, + @JsonProperty("warnings") final Collection warnings) { this.courses = courses; this.warnings = warnings; } @@ -300,8 +299,8 @@ public abstract class MoodleUtils { @JsonCreator public CourseQuizData( - @JsonProperty(value = "quizzes") final Collection quizzes, - @JsonProperty(value = "warnings") final Collection warnings) { + @JsonProperty("quizzes") final Collection quizzes, + @JsonProperty("warnings") final Collection 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 customfields; - - @JsonCreator - public MoodlePluginUserDetails( - final String id, - final String username, - final String firstname, - final String lastname, - final String idnumber, - final String email, - final Map 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 courseKeys; public final Collection warnings; + @JsonCreator public CoursePage( - @JsonProperty(value = "courses") final Collection courseKeys, - @JsonProperty(value = "warnings") final Collection warnings) { + @JsonProperty("courses") final Collection courseKeys, + @JsonProperty("warnings") final Collection 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; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java index 5d3aef3f..e5c1651f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java @@ -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 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 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 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 cAttributes = new LinkedMultiValueMap<>(); + final List 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() { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java index 6bc7acb5..5ad9b7bd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java @@ -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 getExamineeAccountDetails(final String examineeUserId) { + public Result 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 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 getExamineeAccountDetails(final String examineeSessionId) { return Result.tryCatch(() -> { final MoodleAPIRestTemplate template = getRestTemplate() .getOrThrow(); final MultiValueMap 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. readValue( + final MoodleUserDetails[] userDetails = this.jsonMapper. readValue( userDetailsJSON, - new TypeReference() { + new TypeReference() { }); if (userDetails == null || userDetails.length <= 0) { throw new RuntimeException("No user details on Moodle API request"); } + final Map 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 courseData = new HashMap<>(); - final Collection coursesPage = getCoursesPage(restTemplate, quizFromTime, page, this.pageSize); - - // no courses for page --> finish - if (coursesPage == null || coursesPage.isEmpty()) { + final Collection 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 attributes = new LinkedMultiValueMap<>(); - final List 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 getCoursesPage( + private Collection 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 getQuizzesForIds( final MoodleAPIRestTemplate restTemplate, - final Set quizIds) { + final Set 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 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 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 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 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()); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java index 7cf10680..addf0ff2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java @@ -105,7 +105,10 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI { final LinkedMultiValueMap 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 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 configKeys = Arrays.asList(StringUtils.split( moodleRestriction.config_keys, Constants.LIST_SEPARATOR)); - final List browserExamKeys = Arrays.asList(StringUtils.split( + final List browserExamKeys = new ArrayList<>(Arrays.asList(StringUtils.split( moodleRestriction.browser_exam_keys, - Constants.LIST_SEPARATOR)); + Constants.LIST_SEPARATOR))); final Map 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, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java index d5e68c38..6facdedf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java @@ -220,6 +220,11 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm return quizRequest(id); } + @Override + public Result tryRecoverQuizForExam(final Exam exam) { + return Result.ofError(new UnsupportedOperationException("Recovering not supported")); + } + @Override public Result getExamineeAccountDetails(final String examineeUserId) { return getRestTemplate().map(t -> this.getExamineeById(t, examineeUserId)); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java index f0362029..c92b9035 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java @@ -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"); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 4e9712c5..b7740a48 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -528,7 +528,7 @@ public class ExamAdministrationController extends EntityController { 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 { return Result.tryCatch(() -> { this.examSessionService.flushCache(entity); return entity; - }).flatMap(this.examAdminService::saveLMSAttributes); + }); } @Override diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 80c2e107..4682470c 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -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