Added Moodle quiz recovery and delete additional attributes for exam
This commit is contained in:
parent
a222590cad
commit
24131ddfac
5 changed files with 137 additions and 14 deletions
|
@ -26,6 +26,7 @@ import java.util.stream.Collectors;
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
import org.mybatis.dynamic.sql.SqlBuilder;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
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.ExamStatus;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
|
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.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.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
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.ClientConnectionRecordMapper;
|
||||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport;
|
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.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.datalayer.batis.model.ExamRecord;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
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.ResourceNotFoundException;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
|
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.LmsAPIService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleCourseAccess;
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
@Component
|
@Component
|
||||||
@WebServiceProfile
|
@WebServiceProfile
|
||||||
public class ExamDAOImpl implements ExamDAO {
|
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 ExamRecordMapper examRecordMapper;
|
||||||
private final ClientConnectionRecordMapper clientConnectionRecordMapper;
|
private final ClientConnectionRecordMapper clientConnectionRecordMapper;
|
||||||
|
private final AdditionalAttributeRecordMapper additionalAttributeRecordMapper;
|
||||||
private final LmsAPIService lmsAPIService;
|
private final LmsAPIService lmsAPIService;
|
||||||
|
|
||||||
public ExamDAOImpl(
|
public ExamDAOImpl(
|
||||||
final ExamRecordMapper examRecordMapper,
|
final ExamRecordMapper examRecordMapper,
|
||||||
final ClientConnectionRecordMapper clientConnectionRecordMapper,
|
final ClientConnectionRecordMapper clientConnectionRecordMapper,
|
||||||
|
final AdditionalAttributeRecordMapper additionalAttributeRecordMapper,
|
||||||
final LmsAPIService lmsAPIService) {
|
final LmsAPIService lmsAPIService) {
|
||||||
|
|
||||||
this.examRecordMapper = examRecordMapper;
|
this.examRecordMapper = examRecordMapper;
|
||||||
this.clientConnectionRecordMapper = clientConnectionRecordMapper;
|
this.clientConnectionRecordMapper = clientConnectionRecordMapper;
|
||||||
|
this.additionalAttributeRecordMapper = additionalAttributeRecordMapper;
|
||||||
this.lmsAPIService = lmsAPIService;
|
this.lmsAPIService = lmsAPIService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,10 +600,16 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
.build()
|
.build()
|
||||||
.execute();
|
.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()
|
return ids.stream()
|
||||||
.map(id -> new EntityKey(id, EntityType.EXAM))
|
.map(id -> new EntityKey(id, EntityType.EXAM))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -774,7 +792,9 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
// collect Exam's
|
// collect Exam's
|
||||||
return recordMapping.entrySet()
|
return recordMapping.entrySet()
|
||||||
.stream()
|
.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(
|
.onError(error -> log.error(
|
||||||
"Failed to get quiz data from remote LMS for exam: ",
|
"Failed to get quiz data from remote LMS for exam: ",
|
||||||
error))
|
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(
|
private Result<Exam> toDomainModel(
|
||||||
final ExamRecord record,
|
final ExamRecord record,
|
||||||
final QuizData quizData) {
|
final QuizData quizData) {
|
||||||
|
@ -807,8 +896,8 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
record.getInstitutionId(),
|
record.getInstitutionId(),
|
||||||
record.getLmsSetupId(),
|
record.getLmsSetupId(),
|
||||||
record.getExternalId(),
|
record.getExternalId(),
|
||||||
(quizData != null) ? quizData.name : "[Failed to load quiz data]",
|
(quizData != null) ? quizData.name : FAILED_TO_LOAD_QUIZ_DATA_MARK,
|
||||||
(quizData != null) ? quizData.description : "[Failed to load quiz data]",
|
(quizData != null) ? quizData.description : FAILED_TO_LOAD_QUIZ_DATA_MARK,
|
||||||
(quizData != null) ? quizData.startTime : null,
|
(quizData != null) ? quizData.startTime : null,
|
||||||
(quizData != null) ? quizData.endTime : null,
|
(quizData != null) ? quizData.endTime : null,
|
||||||
(quizData != null) ? quizData.startURL : Constants.EMPTY_NOTE,
|
(quizData != null) ? quizData.startURL : Constants.EMPTY_NOTE,
|
||||||
|
|
|
@ -19,9 +19,15 @@ public interface ExamAdminService {
|
||||||
/** Adds a default indicator that is defined by configuration to a given exam.
|
/** Adds a default indicator that is defined by configuration to a given exam.
|
||||||
*
|
*
|
||||||
* @param exam The Exam to add the default indicator
|
* @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);
|
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
|
/** Applies all additional SEB restriction attributes that are defined by the
|
||||||
* type of the LMS of a given Exam to this given Exam.
|
* type of the LMS of a given Exam to this given Exam.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.OpenEdxSEBRestriction;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
|
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.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;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
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.dao.IndicatorDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
|
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.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.SEBRestrictionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamProctoringServiceFactory;
|
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
|
@Override
|
||||||
public Result<Boolean> isRestricted(final Exam exam) {
|
public Result<Boolean> isRestricted(final Exam exam) {
|
||||||
if (exam == null) {
|
if (exam == null) {
|
||||||
|
|
|
@ -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);
|
final StringBuilder sb = new StringBuilder(quizId);
|
||||||
if (StringUtils.isNotEmpty(shortname)) {
|
if (StringUtils.isNotEmpty(shortname)) {
|
||||||
sb.insert(0, ":").insert(0, shortname);
|
sb.insert(0, ":").insert(0, shortname);
|
||||||
|
@ -324,7 +324,7 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final String getQuizId(final String internalQuizId) {
|
public static final String getQuizId(final String internalQuizId) {
|
||||||
if (StringUtils.isBlank(internalQuizId)) {
|
if (StringUtils.isBlank(internalQuizId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -333,7 +333,7 @@ public class MoodleCourseAccess extends CourseAccess {
|
||||||
return ids[ids.length - 1];
|
return ids[ids.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
static final String getShortname(final String internalQuizId) {
|
public static final String getShortname(final String internalQuizId) {
|
||||||
if (StringUtils.isBlank(internalQuizId)) {
|
if (StringUtils.isBlank(internalQuizId)) {
|
||||||
return null;
|
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)) {
|
if (StringUtils.isBlank(internalQuizId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -450,6 +450,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
protected Result<Exam> notifyCreated(final Exam entity) {
|
protected Result<Exam> notifyCreated(final Exam entity) {
|
||||||
return this.examAdminService
|
return this.examAdminService
|
||||||
.addDefaultIndicator(entity)
|
.addDefaultIndicator(entity)
|
||||||
|
.flatMap(this.examAdminService::saveAdditionalAttributes)
|
||||||
.flatMap(this.examAdminService::applyAdditionalSEBRestrictions);
|
.flatMap(this.examAdminService::applyAdditionalSEBRestrictions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,7 +459,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
this.examSessionService.flushCache(entity);
|
this.examSessionService.flushCache(entity);
|
||||||
return entity;
|
return entity;
|
||||||
});
|
}).flatMap(this.examAdminService::saveAdditionalAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Reference in a new issue