Added Moodle quiz recovery and delete additional attributes for exam

This commit is contained in:
anhefti 2020-10-21 12:20:39 +02:00
parent a222590cad
commit 24131ddfac
5 changed files with 137 additions and 14 deletions

View file

@ -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,7 +792,9 @@ public class ExamDAOImpl implements ExamDAO {
// collect Exam's
return recordMapping.entrySet()
.stream()
.map(entry -> toDomainModel(entry.getValue(), quizzes.get(entry.getKey()))
.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))
@ -784,6 +804,75 @@ public class ExamDAOImpl implements ExamDAO {
});
}
private QuizData getQuizData(
final Map<String, QuizData> 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<Exam> 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,

View file

@ -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<Exam> 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<Exam> 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.
*

View file

@ -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<Exam> saveAdditionalAttributes(final Exam exam) {
return saveAdditionalAttributesForMoodleExams(exam);
}
private Result<Exam> 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<Boolean> isRestricted(final Exam exam) {
if (exam == null) {

View file

@ -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;
}

View file

@ -450,6 +450,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
protected Result<Exam> 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<Exam, Exam> {
return Result.tryCatch(() -> {
this.examSessionService.flushCache(entity);
return entity;
});
}).flatMap(this.examAdminService::saveAdditionalAttributes);
}
@Override