improved LMS fail handling on exams
1. if LMS is not available the exams gets a state override and is not running 2. if LMS lms is available but the course id is invalid, the exams gets a state override and is not running
This commit is contained in:
parent
527c005702
commit
2f8913f129
17 changed files with 150 additions and 91 deletions
|
@ -52,7 +52,10 @@ public class APIMessage implements Serializable {
|
|||
EXAM_CONSISTENCY_VALIDATION_CONFIG("1401", HttpStatus.OK, "No SEB Exam Configuration defined for the Exam"),
|
||||
EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION("1402", HttpStatus.OK,
|
||||
"SEB restriction API available but Exam not restricted on LMS side yet"),
|
||||
EXAM_CONSISTENCY_VALIDATION_INDICATOR("1403", HttpStatus.OK, "No Indicator defined for the Exam");
|
||||
EXAM_CONSISTENCY_VALIDATION_INDICATOR("1403", HttpStatus.OK, "No Indicator defined for the Exam"),
|
||||
EXAM_CONSISTENCY_VALIDATION_LMS_CONNECTION("1404", HttpStatus.OK, "No Connection To LMS"),
|
||||
EXAM_CONSISTENCY_VALIDATION_INVALID_ID_REFERENCE("1405", HttpStatus.OK,
|
||||
"There seems to be an invalid exam - course identifier reference. The course cannot be found");
|
||||
|
||||
public final String messageCode;
|
||||
public final HttpStatus httpStatus;
|
||||
|
|
|
@ -59,7 +59,9 @@ public final class Exam implements GrantEntity {
|
|||
public enum ExamStatus {
|
||||
UP_COMING,
|
||||
RUNNING,
|
||||
FINISHED
|
||||
FINISHED,
|
||||
CORRUPT_NO_LMS_CONNECTION,
|
||||
CORRUPT_INVALID_ID
|
||||
}
|
||||
|
||||
public enum ExamType {
|
||||
|
|
|
@ -82,6 +82,10 @@ public final class LmsSetupTestResult {
|
|||
.anyMatch(error -> error.errorType == type);
|
||||
}
|
||||
|
||||
public boolean hasAnyError() {
|
||||
return !this.errors.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
|
|
@ -35,11 +35,8 @@ 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.ProctoringSettings;
|
||||
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.Features;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
||||
|
@ -62,7 +59,6 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRest
|
|||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizData;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.ImportAsExam;
|
||||
|
@ -79,7 +75,7 @@ public class ExamForm implements TemplateComposer {
|
|||
private static final Logger log = LoggerFactory.getLogger(ExamForm.class);
|
||||
|
||||
protected static final String ATTR_READ_GRANT = "ATTR_READ_GRANT";
|
||||
protected static final String ATTR_MODIFY_GRANT = "ATTR_MODIFY_GRANT";
|
||||
protected static final String ATTR_EDITABLE = "ATTR_EDITABLE";
|
||||
protected static final String ATTR_EXAM_STATUS = "ATTR_EXAM_STATUS";
|
||||
|
||||
public static final LocTextKey EXAM_FORM_TITLE_KEY =
|
||||
|
@ -118,6 +114,10 @@ public class ExamForm implements TemplateComposer {
|
|||
new LocTextKey("sebserver.exam.consistency.missing-config");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_SEB_RESTRICTION =
|
||||
new LocTextKey("sebserver.exam.consistency.missing-seb-restriction");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_VALIDATION_LMS_CONNECTION =
|
||||
new LocTextKey("sebserver.exam.consistency.no-lms-connection");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGEINVALID_ID_REFERENCE =
|
||||
new LocTextKey("sebserver.exam.consistency.invalid-lms-id");
|
||||
|
||||
private final Map<String, LocTextKey> consistencyMessageMapping;
|
||||
private final PageService pageService;
|
||||
|
@ -166,6 +166,12 @@ public class ExamForm implements TemplateComposer {
|
|||
this.consistencyMessageMapping.put(
|
||||
APIMessage.ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION.messageCode,
|
||||
CONSISTENCY_MESSAGE_MISSING_SEB_RESTRICTION);
|
||||
this.consistencyMessageMapping.put(
|
||||
APIMessage.ErrorMessage.EXAM_CONSISTENCY_VALIDATION_LMS_CONNECTION.messageCode,
|
||||
CONSISTENCY_MESSAGE_VALIDATION_LMS_CONNECTION);
|
||||
this.consistencyMessageMapping.put(
|
||||
APIMessage.ErrorMessage.EXAM_CONSISTENCY_VALIDATION_INVALID_ID_REFERENCE.messageCode,
|
||||
CONSISTENCY_MESSAGEINVALID_ID_REFERENCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -218,9 +224,8 @@ public class ExamForm implements TemplateComposer {
|
|||
final boolean modifyGrant = userGrantCheck.m();
|
||||
final boolean writeGrant = userGrantCheck.w();
|
||||
final ExamStatus examStatus = exam.getStatus();
|
||||
final boolean editable = examStatus == ExamStatus.UP_COMING
|
||||
|| examStatus == ExamStatus.RUNNING
|
||||
&& currentUser.get().hasRole(UserRole.EXAM_ADMIN);
|
||||
final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING ||
|
||||
examStatus == ExamStatus.RUNNING);
|
||||
final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam);
|
||||
final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
|
||||
.getBuilder(CheckSEBRestriction.class)
|
||||
|
@ -387,12 +392,7 @@ public class ExamForm implements TemplateComposer {
|
|||
.newAction(ActionDefinition.EXAM_MODIFY_SEB_RESTRICTION_DETAILS)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService))
|
||||
.withAttribute(
|
||||
ExamSEBRestrictionSettings.PAGE_CONTEXT_ATTR_LMS_TYPE,
|
||||
this.restService.getBuilder(GetLmsSetup.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId))
|
||||
.call()
|
||||
.getOrThrow().lmsType.name())
|
||||
.withAttribute(ExamSEBRestrictionSettings.PAGE_CONTEXT_ATTR_LMS_ID, String.valueOf(exam.lmsSetupId))
|
||||
.withAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY, String.valueOf(!modifyGrant))
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> sebRestrictionAvailable && readonly)
|
||||
|
@ -433,7 +433,7 @@ public class ExamForm implements TemplateComposer {
|
|||
formContext
|
||||
.copyOf(content)
|
||||
.withAttribute(ATTR_READ_GRANT, String.valueOf(userGrantCheck.r()))
|
||||
.withAttribute(ATTR_MODIFY_GRANT, String.valueOf(modifyGrant))
|
||||
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
|
||||
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
|
||||
|
||||
// Indicators
|
||||
|
@ -441,7 +441,7 @@ public class ExamForm implements TemplateComposer {
|
|||
formContext
|
||||
.copyOf(content)
|
||||
.withAttribute(ATTR_READ_GRANT, String.valueOf(userGrantCheck.r()))
|
||||
.withAttribute(ATTR_MODIFY_GRANT, String.valueOf(modifyGrant))
|
||||
.withAttribute(ATTR_EDITABLE, String.valueOf(editable))
|
||||
.withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
|
||||
}
|
||||
}
|
||||
|
@ -467,11 +467,7 @@ public class ExamForm implements TemplateComposer {
|
|||
}
|
||||
|
||||
private boolean testSEBRestrictionAPI(final Exam exam) {
|
||||
final Result<LmsSetup> lmsSetupCall = this.restService.getBuilder(GetLmsSetup.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId))
|
||||
.call();
|
||||
|
||||
if (!lmsSetupCall.hasError() && !lmsSetupCall.get().lmsType.features.contains(Features.SEB_RESTRICTION)) {
|
||||
if (exam.status == ExamStatus.CORRUPT_NO_LMS_CONNECTION || exam.status == ExamStatus.CORRUPT_INVALID_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
|
|||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
||||
import ch.ethz.seb.sebserver.gui.service.ResourceService;
|
||||
|
@ -41,7 +40,6 @@ import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
|
|||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteExamConfigMapping;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingsPage;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
|
||||
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
|
||||
import ch.ethz.seb.sebserver.gui.table.EntityTable;
|
||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
||||
|
@ -89,20 +87,15 @@ public class ExamFormConfigs implements TemplateComposer {
|
|||
|
||||
@Override
|
||||
public void compose(final PageContext pageContext) {
|
||||
final CurrentUser currentUser = this.resourceService.getCurrentUser();
|
||||
final Composite content = pageContext.getParent();
|
||||
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
final boolean modifyGrant = BooleanUtils.toBoolean(
|
||||
pageContext.getAttribute(ExamForm.ATTR_MODIFY_GRANT));
|
||||
final boolean editable = BooleanUtils.toBoolean(
|
||||
pageContext.getAttribute(ExamForm.ATTR_EDITABLE));
|
||||
final boolean readGrant = BooleanUtils.toBoolean(
|
||||
pageContext.getAttribute(ExamForm.ATTR_READ_GRANT));
|
||||
final ExamStatus examStatus = ExamStatus.valueOf(
|
||||
pageContext.getAttribute(ExamForm.ATTR_EXAM_STATUS));
|
||||
final boolean isExamRunning = examStatus == ExamStatus.RUNNING;
|
||||
final boolean editable = examStatus == ExamStatus.UP_COMING
|
||||
|| examStatus == ExamStatus.RUNNING
|
||||
&& currentUser.get().hasRole(UserRole.EXAM_ADMIN);
|
||||
|
||||
// List of SEB Configuration
|
||||
this.widgetFactory.addFormSubContextHeader(
|
||||
|
@ -134,7 +127,7 @@ public class ExamFormConfigs implements TemplateComposer {
|
|||
this.resourceService::localizedExamConfigStatusName)
|
||||
.widthProportion(1))
|
||||
.withDefaultActionIf(
|
||||
() -> true,
|
||||
() -> readGrant,
|
||||
this::viewExamConfigPageAction)
|
||||
|
||||
.withSelectionListener(this.pageService.getSelectionPublisher(
|
||||
|
@ -162,12 +155,12 @@ public class ExamFormConfigs implements TemplateComposer {
|
|||
.withParentEntityKey(entityKey)
|
||||
.withExec(this.examToConfigBindingForm.bindFunction())
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> modifyGrant && editable && !configurationTable.hasAnyContent())
|
||||
.publishIf(() -> editable && !configurationTable.hasAnyContent())
|
||||
|
||||
.newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP)
|
||||
.withParentEntityKey(entityKey)
|
||||
.withEntityKey(configMapKey)
|
||||
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent(), false)
|
||||
.publishIf(() -> readGrant && configurationTable.hasAnyContent(), false)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_CONFIGURATION_DELETE_FROM_LIST)
|
||||
.withEntityKey(entityKey)
|
||||
|
@ -181,7 +174,7 @@ public class ExamFormConfigs implements TemplateComposer {
|
|||
}
|
||||
return null;
|
||||
})
|
||||
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable, false)
|
||||
.publishIf(() -> editable && configurationTable.hasAnyContent() && editable, false)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY)
|
||||
.withSelect(
|
||||
|
|
|
@ -72,8 +72,8 @@ public class ExamFormIndicators implements TemplateComposer {
|
|||
public void compose(final PageContext pageContext) {
|
||||
final Composite content = pageContext.getParent();
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
final boolean modifyGrant = BooleanUtils.toBoolean(
|
||||
pageContext.getAttribute(ExamForm.ATTR_MODIFY_GRANT));
|
||||
final boolean editable = BooleanUtils.toBoolean(
|
||||
pageContext.getAttribute(ExamForm.ATTR_EDITABLE));
|
||||
|
||||
// List of Indicators
|
||||
this.widgetFactory.addFormSubContextHeader(
|
||||
|
@ -111,7 +111,7 @@ public class ExamFormIndicators implements TemplateComposer {
|
|||
.asMarkup()
|
||||
.widthProportion(4))
|
||||
.withDefaultActionIf(
|
||||
() -> modifyGrant,
|
||||
() -> editable,
|
||||
() -> actionBuilder
|
||||
.newAction(ActionDefinition.EXAM_INDICATOR_MODIFY_FROM_LIST)
|
||||
.withParentEntityKey(entityKey)
|
||||
|
@ -132,7 +132,7 @@ public class ExamFormIndicators implements TemplateComposer {
|
|||
indicatorTable::getSelection,
|
||||
PageAction::applySingleSelectionAsEntityKey,
|
||||
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
|
||||
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent(), false)
|
||||
.publishIf(() -> editable && indicatorTable.hasAnyContent(), false)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_INDICATOR_DELETE_FROM_LIST)
|
||||
.withEntityKey(entityKey)
|
||||
|
@ -140,11 +140,11 @@ public class ExamFormIndicators implements TemplateComposer {
|
|||
indicatorTable::getSelection,
|
||||
this::deleteSelectedIndicator,
|
||||
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
|
||||
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent(), false)
|
||||
.publishIf(() -> editable && indicatorTable.hasAnyContent(), false)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_INDICATOR_NEW)
|
||||
.withParentEntityKey(entityKey)
|
||||
.publishIf(() -> modifyGrant);
|
||||
.publishIf(() -> editable);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -254,7 +254,7 @@ public class ExamList implements TemplateComposer {
|
|||
final Exam exam,
|
||||
final PageService pageService) {
|
||||
|
||||
if (exam.getStatus() != ExamStatus.RUNNING) {
|
||||
if (exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.FINISHED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeactivateSE
|
|||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetCourseChapters;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetSEBRestrictionSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveSEBRestriction;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
|
@ -81,7 +82,7 @@ public class ExamSEBRestrictionSettings {
|
|||
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_USER_BANNING_ENABLED =
|
||||
new LocTextKey("sebserver.exam.form.sebrestriction.USER_BANNING_ENABLED");
|
||||
|
||||
static final String PAGE_CONTEXT_ATTR_LMS_TYPE = "ATTR_LMS_TYPE";
|
||||
static final String PAGE_CONTEXT_ATTR_LMS_ID = "ATTR_LMS_ID";
|
||||
|
||||
Function<PageAction, PageAction> settingsFunction(final PageService pageService) {
|
||||
|
||||
|
@ -126,7 +127,7 @@ public class ExamSEBRestrictionSettings {
|
|||
}
|
||||
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
final LmsType lmsType = getLmsType(pageContext);
|
||||
final LmsType lmsType = getLmsType(pageService, pageContext.getAttribute(PAGE_CONTEXT_ATTR_LMS_ID));
|
||||
SEBRestriction bodyValue = null;
|
||||
try {
|
||||
final Form form = formHandle.getForm();
|
||||
|
@ -194,7 +195,9 @@ public class ExamSEBRestrictionSettings {
|
|||
final RestService restService = this.pageService.getRestService();
|
||||
final ResourceService resourceService = this.pageService.getResourceService();
|
||||
final EntityKey entityKey = this.pageContext.getEntityKey();
|
||||
final LmsType lmsType = getLmsType(this.pageContext);
|
||||
final LmsType lmsType = getLmsType(
|
||||
this.pageService,
|
||||
this.pageContext.getAttribute(PAGE_CONTEXT_ATTR_LMS_ID));
|
||||
final boolean isReadonly = BooleanUtils.toBoolean(
|
||||
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
|
||||
|
||||
|
@ -305,9 +308,16 @@ public class ExamSEBRestrictionSettings {
|
|||
|
||||
}
|
||||
|
||||
private LmsType getLmsType(final PageContext pageContext) {
|
||||
private LmsType getLmsType(final PageService pageService, final String lmsSetupId) {
|
||||
try {
|
||||
return LmsType.valueOf(pageContext.getAttribute(PAGE_CONTEXT_ATTR_LMS_TYPE));
|
||||
|
||||
return pageService
|
||||
.getRestService()
|
||||
.getBuilder(GetLmsSetup.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, lmsSetupId)
|
||||
.call()
|
||||
.getOrThrow().lmsType;
|
||||
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
@Transactional(readOnly = true)
|
||||
public Result<GrantEntity> examGrantEntityByPK(final Long id) {
|
||||
return recordById(id)
|
||||
.map(record -> toDomainModel(record, null).getOrThrow());
|
||||
.map(record -> toDomainModel(record, null, null).getOrThrow());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -111,7 +111,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
return Result.tryCatch(() -> this.clientConnectionRecordMapper
|
||||
.selectByPrimaryKey(connectionId))
|
||||
.flatMap(ccRecord -> recordById(ccRecord.getExamId()))
|
||||
.map(record -> toDomainModel(record, null).getOrThrow());
|
||||
.map(record -> toDomainModel(record, null, null).getOrThrow());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -793,17 +793,48 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
final Map<String, QuizData> quizzes = this.lmsAPIService
|
||||
.getLmsAPITemplate(lmsSetupId)
|
||||
.map(template -> getQuizzesFromLMS(template, recordMapping.keySet(), cached))
|
||||
.getOrThrow()
|
||||
.onError(error -> log.error("Failed to get quizzes for exams: ", error))
|
||||
.getOr(Collections.emptyList())
|
||||
.stream()
|
||||
.flatMap(Result::skipOnError)
|
||||
.collect(Collectors.toMap(q -> q.id, Function.identity()));
|
||||
|
||||
if (records.size() != quizzes.size()) {
|
||||
|
||||
// Check if we have LMS connection to verify the source of the exam quiz mismatch
|
||||
final LmsAPITemplate lmsSetup = this.lmsAPIService
|
||||
.getLmsAPITemplate(lmsSetupId)
|
||||
.getOrThrow();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Quizzes size mismatch detected by getting exams quiz data from LMS: {}", lmsSetup);
|
||||
}
|
||||
|
||||
if (lmsSetup.testCourseAccessAPI().hasAnyError()) {
|
||||
// No course access on the LMS. This means we can't get any quizzes from this LMSSetup at the moment
|
||||
// All exams are marked as corrupt because of LMS Setup failure
|
||||
|
||||
log.warn("Failed to get quizzes form LMS Setup. No access to LMS {}", lmsSetup);
|
||||
|
||||
return recordMapping.entrySet()
|
||||
.stream()
|
||||
.map(entry -> toDomainModel(
|
||||
entry.getValue(),
|
||||
null,
|
||||
ExamStatus.CORRUPT_NO_LMS_CONNECTION)
|
||||
.getOr(null))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
// collect Exam's
|
||||
return recordMapping.entrySet()
|
||||
.stream()
|
||||
.map(entry -> toDomainModel(
|
||||
entry.getValue(),
|
||||
getQuizData(quizzes, entry.getKey(), entry.getValue()))
|
||||
getQuizData(quizzes, entry.getKey(), entry.getValue()),
|
||||
ExamStatus.CORRUPT_INVALID_ID)
|
||||
.onError(error -> log.error(
|
||||
"Failed to get quiz data from remote LMS for exam: ",
|
||||
error))
|
||||
|
@ -813,8 +844,11 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
});
|
||||
}
|
||||
|
||||
private Collection<Result<QuizData>> getQuizzesFromLMS(final LmsAPITemplate template, final Set<String> ids,
|
||||
private Collection<Result<QuizData>> getQuizzesFromLMS(
|
||||
final LmsAPITemplate template,
|
||||
final Set<String> ids,
|
||||
final boolean cached) {
|
||||
|
||||
try {
|
||||
return (cached)
|
||||
? template.getQuizzesFromCache(ids)
|
||||
|
@ -840,6 +874,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
// 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
|
||||
try {
|
||||
final LmsSetup lmsSetup = this.lmsAPIService
|
||||
.getLmsSetup(record.getLmsSetupId())
|
||||
|
@ -914,7 +949,8 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
|
||||
private Result<Exam> toDomainModel(
|
||||
final ExamRecord record,
|
||||
final QuizData quizData) {
|
||||
final QuizData quizData,
|
||||
final ExamStatus statusOverride) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
|
@ -943,7 +979,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
ExamType.valueOf(record.getType()),
|
||||
record.getOwner(),
|
||||
supporter,
|
||||
status,
|
||||
(quizData != null) ? status : (statusOverride != null) ? statusOverride : status,
|
||||
record.getBrowserKeys(),
|
||||
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null),
|
||||
record.getLastupdate());
|
||||
|
|
|
@ -191,7 +191,7 @@ public class MoodleCourseAccess extends CourseAccess {
|
|||
if (restTemplateRequest.hasError()) {
|
||||
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
|
||||
this.moodleRestTemplateFactory.knownTokenAccessPaths;
|
||||
log.error(message + " cause: ", restTemplateRequest.getError());
|
||||
log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
|
||||
return LmsSetupTestResult.ofTokenRequestError(message);
|
||||
}
|
||||
|
||||
|
@ -213,14 +213,14 @@ public class MoodleCourseAccess extends CourseAccess {
|
|||
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
|
||||
return () -> getRestTemplate()
|
||||
.map(template -> getQuizzesForIds(template, ids))
|
||||
.getOrThrow();
|
||||
.getOr(Collections.emptyList());
|
||||
|
||||
}
|
||||
|
||||
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
|
||||
return () -> getRestTemplate()
|
||||
.map(template -> collectAllQuizzes(template, filterMap))
|
||||
.getOrThrow();
|
||||
.getOr(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -380,17 +380,7 @@ public class MoodleCourseAccess extends CourseAccess {
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) {
|
||||
log.warn(
|
||||
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
|
||||
this.lmsSetup,
|
||||
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
|
||||
courseQuizData.warnings.size(),
|
||||
courseQuizData.warnings.iterator().next().toString());
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("All warnings from Moodle: {}", courseQuizData.warnings.toString());
|
||||
}
|
||||
}
|
||||
logMoodleWarnings(courseQuizData.warnings);
|
||||
|
||||
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
|
||||
log.error("No quizzes found for ids: {} on LMS; {}", quizIds, this.lmsSetup.name);
|
||||
|
@ -461,17 +451,7 @@ public class MoodleCourseAccess extends CourseAccess {
|
|||
Collections.emptyList();
|
||||
}
|
||||
|
||||
if (courses.warnings != null && !courses.warnings.isEmpty()) {
|
||||
log.warn(
|
||||
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
|
||||
this.lmsSetup,
|
||||
MoodleCourseAccess.MOODLE_COURSE_BY_FIELD_API_FUNCTION_NAME,
|
||||
courses.warnings.size(),
|
||||
courses.warnings.iterator().next().toString());
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("All warnings from Moodle: {}", courses.warnings.toString());
|
||||
}
|
||||
}
|
||||
logMoodleWarnings(courses.warnings);
|
||||
|
||||
if (courses.courses == null || courses.courses.isEmpty()) {
|
||||
log.error("No courses found for ids: {} on LMS: {}", ids, this.lmsSetup.name);
|
||||
|
@ -643,6 +623,21 @@ public class MoodleCourseAccess extends CourseAccess {
|
|||
return idNumber.equals(Constants.EMPTY_NOTE) ? null : idNumber;
|
||||
}
|
||||
|
||||
private void logMoodleWarnings(final Collection<Warning> warnings) {
|
||||
if (warnings != null && !warnings.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"There are warnings from Moodle response: Moodle: {} request: {} warnings: {} warning sample: {}",
|
||||
this.lmsSetup,
|
||||
MoodleCourseAccess.MOODLE_QUIZ_API_FUNCTION_NAME,
|
||||
warnings.size(),
|
||||
warnings.iterator().next().toString());
|
||||
} else if (log.isTraceEnabled()) {
|
||||
log.trace("All warnings from Moodle: {}", warnings.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final Pattern ACCESS_DENIED_PATTERN_1 =
|
||||
Pattern.compile(Pattern.quote("No access rights"), Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ACCESS_DENIED_PATTERN_2 =
|
||||
|
|
|
@ -117,7 +117,7 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
|
|||
@Override
|
||||
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Get SEB Client restriction for Exam: {}", exam);
|
||||
log.debug("Get SEB Client restriction for Exam: {}", exam.externalId);
|
||||
}
|
||||
|
||||
return this.moodleCourseRestriction
|
||||
|
|
|
@ -128,14 +128,18 @@ class MoodleRestTemplateFactory {
|
|||
.map(this::createRestTemplate)
|
||||
.map(result -> {
|
||||
if (result.hasError()) {
|
||||
log.error("Failed to get access token: ", result.getError());
|
||||
log.warn("Failed to get access token for LMS: {}({})",
|
||||
this.lmsSetup.name,
|
||||
this.lmsSetup.id);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.filter(Result::hasValue)
|
||||
.findFirst()
|
||||
.orElse(Result.ofRuntimeError(
|
||||
"Failed to gain any access on paths: " + this.knownTokenAccessPaths));
|
||||
"Failed to gain any access for LMS " +
|
||||
this.lmsSetup.name + "(" + this.lmsSetup.id +
|
||||
") on paths: " + this.knownTokenAccessPaths));
|
||||
}
|
||||
|
||||
Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) {
|
||||
|
@ -146,7 +150,9 @@ class MoodleRestTemplateFactory {
|
|||
|
||||
final CharSequence accessToken = template.getAccessToken();
|
||||
if (accessToken == null) {
|
||||
throw new RuntimeException("Failed to gain access token on path: " + accessTokenPath);
|
||||
throw new RuntimeException("Failed to get access token for LMS " +
|
||||
this.lmsSetup.name + "(" + this.lmsSetup.id +
|
||||
") on path: " + accessTokenPath);
|
||||
}
|
||||
|
||||
return template;
|
||||
|
|
|
@ -64,6 +64,7 @@ public interface ExamSessionService {
|
|||
/** Use this to check the consistency of a running Exam.
|
||||
* Current consistency checks are:
|
||||
* - Check if there is at least one Exam supporter attached to the Exam
|
||||
* - Check if we have access to LMS for the exam
|
||||
* - Check if there is one default SEB Exam Configuration attached to the Exam
|
||||
* - Check if SEB restriction API is available and the exam is running but not yet restricted on LMS side
|
||||
* - Check if there is at least one Indicator defined for the monitoring of the Exam
|
||||
|
@ -71,7 +72,7 @@ public interface ExamSessionService {
|
|||
* @param examId the identifier of the Exam to check
|
||||
* @return Result of one APIMessage per consistency check if the check failed. An empty Collection of everything is
|
||||
* okay. */
|
||||
Result<Collection<APIMessage>> checkRunningExamConsistency(Long examId);
|
||||
Result<Collection<APIMessage>> checkExamConsistency(Long examId);
|
||||
|
||||
/** Use this to check if a specified Exam has currently active SEB Client connections.
|
||||
*
|
||||
|
|
|
@ -133,7 +133,8 @@ public class ExamSessionCacheService {
|
|||
case RUNNING: {
|
||||
return true;
|
||||
}
|
||||
case UP_COMING: {
|
||||
case UP_COMING:
|
||||
case FINISHED: {
|
||||
return this.examUpdateHandler.updateRunning(exam.id)
|
||||
.map(e -> e.status == ExamStatus.RUNNING)
|
||||
.getOr(false);
|
||||
|
|
|
@ -121,14 +121,22 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Result<Collection<APIMessage>> checkRunningExamConsistency(final Long examId) {
|
||||
public Result<Collection<APIMessage>> checkExamConsistency(final Long examId) {
|
||||
return Result.tryCatch(() -> {
|
||||
final Collection<APIMessage> result = new ArrayList<>();
|
||||
|
||||
if (isExamRunning(examId)) {
|
||||
final Exam exam = getRunningExam(examId)
|
||||
.getOrThrow();
|
||||
final Exam exam = this.examDAO.byPK(examId)
|
||||
.getOrThrow();
|
||||
|
||||
// check lms connection
|
||||
if (exam.status == ExamStatus.CORRUPT_NO_LMS_CONNECTION) {
|
||||
result.add(ErrorMessage.EXAM_CONSISTENCY_VALIDATION_LMS_CONNECTION.of(exam.getModelId()));
|
||||
}
|
||||
if (exam.status == ExamStatus.CORRUPT_INVALID_ID) {
|
||||
result.add(ErrorMessage.EXAM_CONSISTENCY_VALIDATION_INVALID_ID_REFERENCE.of(exam.getModelId()));
|
||||
}
|
||||
|
||||
if (exam.status == ExamStatus.RUNNING) {
|
||||
// check exam supporter
|
||||
if (exam.getSupporter().isEmpty()) {
|
||||
result.add(ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SUPPORTER.of(exam.getModelId()));
|
||||
|
|
|
@ -246,7 +246,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
|
||||
checkReadPrivilege(institutionId);
|
||||
return this.examSessionService
|
||||
.checkRunningExamConsistency(modelId)
|
||||
.checkExamConsistency(modelId)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
|
|
|
@ -430,6 +430,8 @@ sebserver.exam.consistency.missing-supporter= - There are no Exam Supporter defi
|
|||
sebserver.exam.consistency.missing-indicator= - There is no indicator defined for this exam. Use 'Add Indicator" on the right to add an indicator.
|
||||
sebserver.exam.consistency.missing-config= - There is no configuration defined for this exam. Use 'Add Configuration' to attach one.
|
||||
sebserver.exam.consistency.missing-seb-restriction= - There is currently no SEB restriction applied on the LMS side. Use 'Enable SEB Restriction' on the right to activate auto-restriction.<br/> Or if this is not possible consider doing it manually on the LMS.
|
||||
sebserver.exam.consistency.no-lms-connection= - Failed to connect to the LMS Setup of this exam yet.<br/>Please check the LMS connection within the LMS Setup.
|
||||
sebserver.exam.consistency.invalid-lms-id= - The referencing course identifier seems to be invalid.<br/>Please check if the course for this exam still exists on the LMS and the course identifier has not changed.
|
||||
sebserver.exam.confirm.remove-config=This exam is current running. The remove of the attached configuration will led to an invalid state<br/>where connecting SEB clients cannot download the configuration for the exam.<br/><br/>Are you sure to remove the configuration?
|
||||
|
||||
sebserver.exam.action.list=Exam
|
||||
|
@ -530,6 +532,8 @@ sebserver.exam.type.VDI.tooltip=Exam type specified for Virtual Desktop Infrastr
|
|||
sebserver.exam.status.UP_COMING=Up Coming
|
||||
sebserver.exam.status.RUNNING=Running
|
||||
sebserver.exam.status.FINISHED=Finished
|
||||
sebserver.exam.status.CORRUPT_NO_LMS_CONNECTION=Corrupt (No LMS Connection)
|
||||
sebserver.exam.status.CORRUPT_INVALID_ID=Corrupt (Invalid Identifier)
|
||||
|
||||
sebserver.exam.configuration.list.actions=
|
||||
sebserver.exam.configuration.list.title=Exam Configuration
|
||||
|
|
Loading…
Reference in a new issue