From 24131ddfac45c7d7a48a44c3586125d58cf8ad1d Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 21 Oct 2020 12:20:39 +0200 Subject: [PATCH] Added Moodle quiz recovery and delete additional attributes for exam --- .../servicelayer/dao/impl/ExamDAOImpl.java | 105 ++++++++++++++++-- .../servicelayer/exam/ExamAdminService.java | 8 +- .../exam/impl/ExamAdminServiceImpl.java | 27 +++++ .../lms/impl/moodle/MoodleCourseAccess.java | 8 +- .../api/ExamAdministrationController.java | 3 +- 5 files changed, 137 insertions(+), 14 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index 1657350e..52f6dc44 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -26,6 +26,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; +import org.mybatis.dynamic.sql.SqlBuilder; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; @@ -40,12 +41,17 @@ 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.Exam.ExamType; 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; 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.datalayer.batis.mapper.AdditionalAttributeRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.AdditionalAttributeRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; @@ -53,23 +59,29 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess; @Lazy @Component @WebServiceProfile public class ExamDAOImpl implements ExamDAO { + public static final String FAILED_TO_LOAD_QUIZ_DATA_MARK = "[FAILED TO LOAD DATA FROM LMS]"; + private final ExamRecordMapper examRecordMapper; private final ClientConnectionRecordMapper clientConnectionRecordMapper; + private final AdditionalAttributeRecordMapper additionalAttributeRecordMapper; private final LmsAPIService lmsAPIService; public ExamDAOImpl( final ExamRecordMapper examRecordMapper, final ClientConnectionRecordMapper clientConnectionRecordMapper, + final AdditionalAttributeRecordMapper additionalAttributeRecordMapper, final LmsAPIService lmsAPIService) { this.examRecordMapper = examRecordMapper; this.clientConnectionRecordMapper = clientConnectionRecordMapper; + this.additionalAttributeRecordMapper = additionalAttributeRecordMapper; this.lmsAPIService = lmsAPIService; } @@ -588,10 +600,16 @@ public class ExamDAOImpl implements ExamDAO { .build() .execute(); + // delete all additional attributes + this.additionalAttributeRecordMapper.deleteByExample() + .where(AdditionalAttributeRecordDynamicSqlSupport.entityType, isEqualTo(EntityType.EXAM.name())) + .and(AdditionalAttributeRecordDynamicSqlSupport.entityId, isIn(ids)) + .build() + .execute(); + return ids.stream() .map(id -> new EntityKey(id, EntityType.EXAM)) .collect(Collectors.toList()); - }); } @@ -774,16 +792,87 @@ public class ExamDAOImpl implements ExamDAO { // collect Exam's return recordMapping.entrySet() .stream() - .map(entry -> toDomainModel(entry.getValue(), quizzes.get(entry.getKey())) - .onError(error -> log.error( - "Failed to get quiz data from remote LMS for exam: ", - error)) - .getOr(null)) + .map(entry -> toDomainModel( + entry.getValue(), + getQuizData(quizzes, entry.getKey(), entry.getValue())) + .onError(error -> log.error( + "Failed to get quiz data from remote LMS for exam: ", + error)) + .getOr(null)) .filter(Objects::nonNull) .collect(Collectors.toList()); }); } + private QuizData getQuizData( + final Map quizzes, + final String externalId, + final ExamRecord record) { + + if (quizzes.containsKey(externalId)) { + return quizzes.get(externalId); + } else { + // 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. + try { + final LmsSetup lmsSetup = this.lmsAPIService.getLmsSetup(record.getLmsSetupId()) + .getOrThrow(); + if (lmsSetup.lmsType == LmsType.MOODLE) { + // get additional quiz name attribute + final AdditionalAttributeRecord additionalAttribute = + this.additionalAttributeRecordMapper.selectByExample() + .where( + AdditionalAttributeRecordDynamicSqlSupport.entityType, + SqlBuilder.isEqualTo(EntityType.EXAM.name())) + .and( + AdditionalAttributeRecordDynamicSqlSupport.entityId, + SqlBuilder.isEqualTo(record.getId())) + .and( + AdditionalAttributeRecordDynamicSqlSupport.name, + SqlBuilder.isEqualTo(QuizData.QUIZ_ATTR_NAME)) + .build() + .execute() + .stream() + .findAny() + .orElse(null); + if (additionalAttribute != null) { + // get the course name identifier + final String shortname = MoodleCourseAccess.getShortname(externalId); + if (StringUtils.isNotBlank(shortname)) { + final QuizData recoveredQuizData = quizzes.entrySet() + .stream() + .filter(quizEntry -> { + final String qShortName = MoodleCourseAccess.getShortname(quizEntry.getKey()); + return qShortName != null && qShortName.equals(shortname); + }) + .map(quizEntry -> quizEntry.getValue()) + .filter(quiz -> additionalAttribute.getValue().equals(quiz.name)) + .findAny() + .orElse(null); + if (recoveredQuizData != null) { + // save exam with new external id + this.examRecordMapper.updateByPrimaryKeySelective(new ExamRecord( + record.getId(), + null, null, + recoveredQuizData.id, + null, null, null, null, null, null, null, null, null, null)); + } + return recoveredQuizData; + } + } + } + } catch (final Exception e) { + log.warn("Failed to try to recover from Moodle quiz restore: ", e.getMessage()); + } + return null; + } + } + private Result toDomainModel( final ExamRecord record, final QuizData quizData) { @@ -807,8 +896,8 @@ public class ExamDAOImpl implements ExamDAO { record.getInstitutionId(), record.getLmsSetupId(), record.getExternalId(), - (quizData != null) ? quizData.name : "[Failed to load quiz data]", - (quizData != null) ? quizData.description : "[Failed to load quiz data]", + (quizData != null) ? quizData.name : FAILED_TO_LOAD_QUIZ_DATA_MARK, + (quizData != null) ? quizData.description : FAILED_TO_LOAD_QUIZ_DATA_MARK, (quizData != null) ? quizData.startTime : null, (quizData != null) ? quizData.endTime : null, (quizData != null) ? quizData.startURL : Constants.EMPTY_NOTE, 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 256efd77..e9c9f092 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 @@ -19,9 +19,15 @@ public interface ExamAdminService { /** Adds a default indicator that is defined by configuration to a given exam. * * @param exam The Exam to add the default indicator - * @return the Exam with added default indicator */ + * @return Result refer to the Exam with added default indicator or to an error if happened */ Result addDefaultIndicator(Exam exam); + /** Saves additional attributes for a specified Exam on creation or on update. + * + * @param exam The Exam to add the default indicator + * @return Result refer */ + Result saveAdditionalAttributes(Exam exam); + /** Applies all additional SEB restriction attributes that are defined by the * type of the LMS of a given Exam to this given Exam. * 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 4f076997..c7353fb2 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 @@ -33,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType; +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; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -44,6 +45,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; 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.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.session.ExamProctoringService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamProctoringServiceFactory; @@ -145,6 +147,31 @@ public class ExamAdminServiceImpl implements ExamAdminService { }); } + @Override + public Result saveAdditionalAttributes(final Exam exam) { + return saveAdditionalAttributesForMoodleExams(exam); + } + + private Result saveAdditionalAttributesForMoodleExams(final Exam exam) { + return Result.tryCatch(() -> { + final LmsAPITemplate lmsTemplate = this.lmsAPIService + .getLmsAPITemplate(exam.lmsSetupId) + .getOrThrow(); + + if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE) { + lmsTemplate.getQuiz(exam.externalId) + .flatMap(quizData -> this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + exam.id, + QuizData.QUIZ_ATTR_NAME, + quizData.name)) + .getOrThrow(); + } + + return exam; + }); + } + @Override public Result isRestricted(final Exam exam) { if (exam == null) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java index 23917a86..27f47eec 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java @@ -313,7 +313,7 @@ public class MoodleCourseAccess extends CourseAccess { } - static final String getInternalQuizId(final String quizId, final String shortname, final String idnumber) { + public static final String getInternalQuizId(final String quizId, final String shortname, final String idnumber) { final StringBuilder sb = new StringBuilder(quizId); if (StringUtils.isNotEmpty(shortname)) { sb.insert(0, ":").insert(0, shortname); @@ -324,7 +324,7 @@ public class MoodleCourseAccess extends CourseAccess { return sb.toString(); } - static final String getQuizId(final String internalQuizId) { + public static final String getQuizId(final String internalQuizId) { if (StringUtils.isBlank(internalQuizId)) { return null; } @@ -333,7 +333,7 @@ public class MoodleCourseAccess extends CourseAccess { return ids[ids.length - 1]; } - static final String getShortname(final String internalQuizId) { + public static final String getShortname(final String internalQuizId) { if (StringUtils.isBlank(internalQuizId)) { return null; } @@ -346,7 +346,7 @@ public class MoodleCourseAccess extends CourseAccess { } } - static final String getIdnumber(final String internalQuizId) { + public static final String getIdnumber(final String internalQuizId) { if (StringUtils.isBlank(internalQuizId)) { return null; } 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 27e796cb..162249c1 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 @@ -450,6 +450,7 @@ public class ExamAdministrationController extends EntityController { protected Result notifyCreated(final Exam entity) { return this.examAdminService .addDefaultIndicator(entity) + .flatMap(this.examAdminService::saveAdditionalAttributes) .flatMap(this.examAdminService::applyAdditionalSEBRestrictions); } @@ -458,7 +459,7 @@ public class ExamAdministrationController extends EntityController { return Result.tryCatch(() -> { this.examSessionService.flushCache(entity); return entity; - }); + }).flatMap(this.examAdminService::saveAdditionalAttributes); } @Override