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:
anhefti 2021-03-03 13:28:33 +01:00
parent 527c005702
commit 2f8913f129
17 changed files with 150 additions and 91 deletions

View file

@ -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_CONFIG("1401", HttpStatus.OK, "No SEB Exam Configuration defined for the Exam"),
EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION("1402", HttpStatus.OK, EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION("1402", HttpStatus.OK,
"SEB restriction API available but Exam not restricted on LMS side yet"), "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 String messageCode;
public final HttpStatus httpStatus; public final HttpStatus httpStatus;

View file

@ -59,7 +59,9 @@ public final class Exam implements GrantEntity {
public enum ExamStatus { public enum ExamStatus {
UP_COMING, UP_COMING,
RUNNING, RUNNING,
FINISHED FINISHED,
CORRUPT_NO_LMS_CONNECTION,
CORRUPT_INVALID_ID
} }
public enum ExamType { public enum ExamType {

View file

@ -82,6 +82,10 @@ public final class LmsSetupTestResult {
.anyMatch(error -> error.errorType == type); .anyMatch(error -> error.errorType == type);
} }
public boolean hasAnyError() {
return !this.errors.isEmpty();
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();

View file

@ -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.Exam.ExamStatus;
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.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.Features;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; 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.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.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; 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.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings; 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.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.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.GetQuizData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.ImportAsExam; 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); private static final Logger log = LoggerFactory.getLogger(ExamForm.class);
protected static final String ATTR_READ_GRANT = "ATTR_READ_GRANT"; 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"; protected static final String ATTR_EXAM_STATUS = "ATTR_EXAM_STATUS";
public static final LocTextKey EXAM_FORM_TITLE_KEY = public static final LocTextKey EXAM_FORM_TITLE_KEY =
@ -118,6 +114,10 @@ public class ExamForm implements TemplateComposer {
new LocTextKey("sebserver.exam.consistency.missing-config"); new LocTextKey("sebserver.exam.consistency.missing-config");
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_SEB_RESTRICTION = private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_SEB_RESTRICTION =
new LocTextKey("sebserver.exam.consistency.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 Map<String, LocTextKey> consistencyMessageMapping;
private final PageService pageService; private final PageService pageService;
@ -166,6 +166,12 @@ public class ExamForm implements TemplateComposer {
this.consistencyMessageMapping.put( this.consistencyMessageMapping.put(
APIMessage.ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION.messageCode, APIMessage.ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION.messageCode,
CONSISTENCY_MESSAGE_MISSING_SEB_RESTRICTION); 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 @Override
@ -218,9 +224,8 @@ public class ExamForm implements TemplateComposer {
final boolean modifyGrant = userGrantCheck.m(); final boolean modifyGrant = userGrantCheck.m();
final boolean writeGrant = userGrantCheck.w(); final boolean writeGrant = userGrantCheck.w();
final ExamStatus examStatus = exam.getStatus(); final ExamStatus examStatus = exam.getStatus();
final boolean editable = examStatus == ExamStatus.UP_COMING final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING ||
|| examStatus == ExamStatus.RUNNING examStatus == ExamStatus.RUNNING);
&& currentUser.get().hasRole(UserRole.EXAM_ADMIN);
final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam); final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam);
final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
.getBuilder(CheckSEBRestriction.class) .getBuilder(CheckSEBRestriction.class)
@ -387,12 +392,7 @@ public class ExamForm implements TemplateComposer {
.newAction(ActionDefinition.EXAM_MODIFY_SEB_RESTRICTION_DETAILS) .newAction(ActionDefinition.EXAM_MODIFY_SEB_RESTRICTION_DETAILS)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService)) .withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService))
.withAttribute( .withAttribute(ExamSEBRestrictionSettings.PAGE_CONTEXT_ATTR_LMS_ID, String.valueOf(exam.lmsSetupId))
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(PageContext.AttributeKeys.FORCE_READ_ONLY, String.valueOf(!modifyGrant)) .withAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY, String.valueOf(!modifyGrant))
.noEventPropagation() .noEventPropagation()
.publishIf(() -> sebRestrictionAvailable && readonly) .publishIf(() -> sebRestrictionAvailable && readonly)
@ -433,7 +433,7 @@ public class ExamForm implements TemplateComposer {
formContext formContext
.copyOf(content) .copyOf(content)
.withAttribute(ATTR_READ_GRANT, String.valueOf(userGrantCheck.r())) .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())); .withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
// Indicators // Indicators
@ -441,7 +441,7 @@ public class ExamForm implements TemplateComposer {
formContext formContext
.copyOf(content) .copyOf(content)
.withAttribute(ATTR_READ_GRANT, String.valueOf(userGrantCheck.r())) .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())); .withAttribute(ATTR_EXAM_STATUS, examStatus.name()));
} }
} }
@ -467,11 +467,7 @@ public class ExamForm implements TemplateComposer {
} }
private boolean testSEBRestrictionAPI(final Exam exam) { private boolean testSEBRestrictionAPI(final Exam exam) {
final Result<LmsSetup> lmsSetupCall = this.restService.getBuilder(GetLmsSetup.class) if (exam.status == ExamStatus.CORRUPT_NO_LMS_CONNECTION || exam.status == ExamStatus.CORRUPT_INVALID_ID) {
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId))
.call();
if (!lmsSetupCall.hasError() && !lmsSetupCall.get().lmsType.features.contains(Features.SEB_RESTRICTION)) {
return false; return false;
} }

View file

@ -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.EntityKey;
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.ExamConfigurationMap; 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.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.service.ResourceService; 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.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteExamConfigMapping; 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.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.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.EntityTable; import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@ -89,20 +87,15 @@ public class ExamFormConfigs implements TemplateComposer {
@Override @Override
public void compose(final PageContext pageContext) { public void compose(final PageContext pageContext) {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final Composite content = pageContext.getParent(); final Composite content = pageContext.getParent();
final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey entityKey = pageContext.getEntityKey();
final boolean modifyGrant = BooleanUtils.toBoolean( final boolean editable = BooleanUtils.toBoolean(
pageContext.getAttribute(ExamForm.ATTR_MODIFY_GRANT)); pageContext.getAttribute(ExamForm.ATTR_EDITABLE));
final boolean readGrant = BooleanUtils.toBoolean( final boolean readGrant = BooleanUtils.toBoolean(
pageContext.getAttribute(ExamForm.ATTR_READ_GRANT)); pageContext.getAttribute(ExamForm.ATTR_READ_GRANT));
final ExamStatus examStatus = ExamStatus.valueOf( final ExamStatus examStatus = ExamStatus.valueOf(
pageContext.getAttribute(ExamForm.ATTR_EXAM_STATUS)); pageContext.getAttribute(ExamForm.ATTR_EXAM_STATUS));
final boolean isExamRunning = examStatus == ExamStatus.RUNNING; 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 // List of SEB Configuration
this.widgetFactory.addFormSubContextHeader( this.widgetFactory.addFormSubContextHeader(
@ -134,7 +127,7 @@ public class ExamFormConfigs implements TemplateComposer {
this.resourceService::localizedExamConfigStatusName) this.resourceService::localizedExamConfigStatusName)
.widthProportion(1)) .widthProportion(1))
.withDefaultActionIf( .withDefaultActionIf(
() -> true, () -> readGrant,
this::viewExamConfigPageAction) this::viewExamConfigPageAction)
.withSelectionListener(this.pageService.getSelectionPublisher( .withSelectionListener(this.pageService.getSelectionPublisher(
@ -162,12 +155,12 @@ public class ExamFormConfigs implements TemplateComposer {
.withParentEntityKey(entityKey) .withParentEntityKey(entityKey)
.withExec(this.examToConfigBindingForm.bindFunction()) .withExec(this.examToConfigBindingForm.bindFunction())
.noEventPropagation() .noEventPropagation()
.publishIf(() -> modifyGrant && editable && !configurationTable.hasAnyContent()) .publishIf(() -> editable && !configurationTable.hasAnyContent())
.newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP) .newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP)
.withParentEntityKey(entityKey) .withParentEntityKey(entityKey)
.withEntityKey(configMapKey) .withEntityKey(configMapKey)
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent(), false) .publishIf(() -> readGrant && configurationTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_DELETE_FROM_LIST) .newAction(ActionDefinition.EXAM_CONFIGURATION_DELETE_FROM_LIST)
.withEntityKey(entityKey) .withEntityKey(entityKey)
@ -181,7 +174,7 @@ public class ExamFormConfigs implements TemplateComposer {
} }
return null; return null;
}) })
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable, false) .publishIf(() -> editable && configurationTable.hasAnyContent() && editable, false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY) .newAction(ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY)
.withSelect( .withSelect(

View file

@ -72,8 +72,8 @@ public class ExamFormIndicators implements TemplateComposer {
public void compose(final PageContext pageContext) { public void compose(final PageContext pageContext) {
final Composite content = pageContext.getParent(); final Composite content = pageContext.getParent();
final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey entityKey = pageContext.getEntityKey();
final boolean modifyGrant = BooleanUtils.toBoolean( final boolean editable = BooleanUtils.toBoolean(
pageContext.getAttribute(ExamForm.ATTR_MODIFY_GRANT)); pageContext.getAttribute(ExamForm.ATTR_EDITABLE));
// List of Indicators // List of Indicators
this.widgetFactory.addFormSubContextHeader( this.widgetFactory.addFormSubContextHeader(
@ -111,7 +111,7 @@ public class ExamFormIndicators implements TemplateComposer {
.asMarkup() .asMarkup()
.widthProportion(4)) .widthProportion(4))
.withDefaultActionIf( .withDefaultActionIf(
() -> modifyGrant, () -> editable,
() -> actionBuilder () -> actionBuilder
.newAction(ActionDefinition.EXAM_INDICATOR_MODIFY_FROM_LIST) .newAction(ActionDefinition.EXAM_INDICATOR_MODIFY_FROM_LIST)
.withParentEntityKey(entityKey) .withParentEntityKey(entityKey)
@ -132,7 +132,7 @@ public class ExamFormIndicators implements TemplateComposer {
indicatorTable::getSelection, indicatorTable::getSelection,
PageAction::applySingleSelectionAsEntityKey, PageAction::applySingleSelectionAsEntityKey,
INDICATOR_EMPTY_SELECTION_TEXT_KEY) INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent(), false) .publishIf(() -> editable && indicatorTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_INDICATOR_DELETE_FROM_LIST) .newAction(ActionDefinition.EXAM_INDICATOR_DELETE_FROM_LIST)
.withEntityKey(entityKey) .withEntityKey(entityKey)
@ -140,11 +140,11 @@ public class ExamFormIndicators implements TemplateComposer {
indicatorTable::getSelection, indicatorTable::getSelection,
this::deleteSelectedIndicator, this::deleteSelectedIndicator,
INDICATOR_EMPTY_SELECTION_TEXT_KEY) INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> modifyGrant && indicatorTable.hasAnyContent(), false) .publishIf(() -> editable && indicatorTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_INDICATOR_NEW) .newAction(ActionDefinition.EXAM_INDICATOR_NEW)
.withParentEntityKey(entityKey) .withParentEntityKey(entityKey)
.publishIf(() -> modifyGrant); .publishIf(() -> editable);
} }

View file

@ -254,7 +254,7 @@ public class ExamList implements TemplateComposer {
final Exam exam, final Exam exam,
final PageService pageService) { final PageService pageService) {
if (exam.getStatus() != ExamStatus.RUNNING) { if (exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.FINISHED) {
return; return;
} }

View file

@ -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.GetCourseChapters;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetSEBRestrictionSettings; 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.exam.SaveSEBRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
@Lazy @Lazy
@Component @Component
@ -81,7 +82,7 @@ public class ExamSEBRestrictionSettings {
private final static LocTextKey SEB_RESTRICTION_FORM_EDX_USER_BANNING_ENABLED = private final static LocTextKey SEB_RESTRICTION_FORM_EDX_USER_BANNING_ENABLED =
new LocTextKey("sebserver.exam.form.sebrestriction.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) { Function<PageAction, PageAction> settingsFunction(final PageService pageService) {
@ -126,7 +127,7 @@ public class ExamSEBRestrictionSettings {
} }
final EntityKey entityKey = pageContext.getEntityKey(); 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; SEBRestriction bodyValue = null;
try { try {
final Form form = formHandle.getForm(); final Form form = formHandle.getForm();
@ -194,7 +195,9 @@ public class ExamSEBRestrictionSettings {
final RestService restService = this.pageService.getRestService(); final RestService restService = this.pageService.getRestService();
final ResourceService resourceService = this.pageService.getResourceService(); final ResourceService resourceService = this.pageService.getResourceService();
final EntityKey entityKey = this.pageContext.getEntityKey(); 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( final boolean isReadonly = BooleanUtils.toBoolean(
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY)); 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 { 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) { } catch (final Exception e) {
return null; return null;
} }

View file

@ -102,7 +102,7 @@ public class ExamDAOImpl implements ExamDAO {
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<GrantEntity> examGrantEntityByPK(final Long id) { public Result<GrantEntity> examGrantEntityByPK(final Long id) {
return recordById(id) return recordById(id)
.map(record -> toDomainModel(record, null).getOrThrow()); .map(record -> toDomainModel(record, null, null).getOrThrow());
} }
@Override @Override
@ -111,7 +111,7 @@ public class ExamDAOImpl implements ExamDAO {
return Result.tryCatch(() -> this.clientConnectionRecordMapper return Result.tryCatch(() -> this.clientConnectionRecordMapper
.selectByPrimaryKey(connectionId)) .selectByPrimaryKey(connectionId))
.flatMap(ccRecord -> recordById(ccRecord.getExamId())) .flatMap(ccRecord -> recordById(ccRecord.getExamId()))
.map(record -> toDomainModel(record, null).getOrThrow()); .map(record -> toDomainModel(record, null, null).getOrThrow());
} }
@Override @Override
@ -793,17 +793,48 @@ public class ExamDAOImpl implements ExamDAO {
final Map<String, QuizData> quizzes = this.lmsAPIService final Map<String, QuizData> quizzes = this.lmsAPIService
.getLmsAPITemplate(lmsSetupId) .getLmsAPITemplate(lmsSetupId)
.map(template -> getQuizzesFromLMS(template, recordMapping.keySet(), cached)) .map(template -> getQuizzesFromLMS(template, recordMapping.keySet(), cached))
.getOrThrow() .onError(error -> log.error("Failed to get quizzes for exams: ", error))
.getOr(Collections.emptyList())
.stream() .stream()
.flatMap(Result::skipOnError) .flatMap(Result::skipOnError)
.collect(Collectors.toMap(q -> q.id, Function.identity())); .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 // collect Exam's
return recordMapping.entrySet() return recordMapping.entrySet()
.stream() .stream()
.map(entry -> toDomainModel( .map(entry -> toDomainModel(
entry.getValue(), entry.getValue(),
getQuizData(quizzes, entry.getKey(), entry.getValue())) getQuizData(quizzes, entry.getKey(), entry.getValue()),
ExamStatus.CORRUPT_INVALID_ID)
.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))
@ -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) { final boolean cached) {
try { try {
return (cached) return (cached)
? template.getQuizzesFromCache(ids) ? 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 // 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 // short name. If one quiz has been found that matches all criteria, we adapt the internal id
// mapping to this quiz. // mapping to this quiz.
// If recovering fails, this returns null and the calling side must handle the lack of quiz data
try { try {
final LmsSetup lmsSetup = this.lmsAPIService final LmsSetup lmsSetup = this.lmsAPIService
.getLmsSetup(record.getLmsSetupId()) .getLmsSetup(record.getLmsSetupId())
@ -914,7 +949,8 @@ public class ExamDAOImpl implements ExamDAO {
private Result<Exam> toDomainModel( private Result<Exam> toDomainModel(
final ExamRecord record, final ExamRecord record,
final QuizData quizData) { final QuizData quizData,
final ExamStatus statusOverride) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
@ -943,7 +979,7 @@ public class ExamDAOImpl implements ExamDAO {
ExamType.valueOf(record.getType()), ExamType.valueOf(record.getType()),
record.getOwner(), record.getOwner(),
supporter, supporter,
status, (quizData != null) ? status : (statusOverride != null) ? statusOverride : status,
record.getBrowserKeys(), record.getBrowserKeys(),
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null), BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : null),
record.getLastupdate()); record.getLastupdate());

View file

@ -191,7 +191,7 @@ public class MoodleCourseAccess extends CourseAccess {
if (restTemplateRequest.hasError()) { if (restTemplateRequest.hasError()) {
final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
this.moodleRestTemplateFactory.knownTokenAccessPaths; this.moodleRestTemplateFactory.knownTokenAccessPaths;
log.error(message + " cause: ", restTemplateRequest.getError()); log.error(message + " cause: {}", restTemplateRequest.getError().getMessage());
return LmsSetupTestResult.ofTokenRequestError(message); return LmsSetupTestResult.ofTokenRequestError(message);
} }
@ -213,14 +213,14 @@ public class MoodleCourseAccess extends CourseAccess {
protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) { protected Supplier<List<QuizData>> quizzesSupplier(final Set<String> ids) {
return () -> getRestTemplate() return () -> getRestTemplate()
.map(template -> getQuizzesForIds(template, ids)) .map(template -> getQuizzesForIds(template, ids))
.getOrThrow(); .getOr(Collections.emptyList());
} }
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) { protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
return () -> getRestTemplate() return () -> getRestTemplate()
.map(template -> collectAllQuizzes(template, filterMap)) .map(template -> collectAllQuizzes(template, filterMap))
.getOrThrow(); .getOr(Collections.emptyList());
} }
@Override @Override
@ -380,17 +380,7 @@ public class MoodleCourseAccess extends CourseAccess {
return Collections.emptyList(); return Collections.emptyList();
} }
if (courseQuizData.warnings != null && !courseQuizData.warnings.isEmpty()) { logMoodleWarnings(courseQuizData.warnings);
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());
}
}
if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) { if (courseQuizData.quizzes == null || courseQuizData.quizzes.isEmpty()) {
log.error("No quizzes found for ids: {} on LMS; {}", quizIds, this.lmsSetup.name); log.error("No quizzes found for ids: {} on LMS; {}", quizIds, this.lmsSetup.name);
@ -461,17 +451,7 @@ public class MoodleCourseAccess extends CourseAccess {
Collections.emptyList(); Collections.emptyList();
} }
if (courses.warnings != null && !courses.warnings.isEmpty()) { logMoodleWarnings(courses.warnings);
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());
}
}
if (courses.courses == null || courses.courses.isEmpty()) { if (courses.courses == null || courses.courses.isEmpty()) {
log.error("No courses found for ids: {} on LMS: {}", ids, this.lmsSetup.name); 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; 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 = private static final Pattern ACCESS_DENIED_PATTERN_1 =
Pattern.compile(Pattern.quote("No access rights"), Pattern.CASE_INSENSITIVE); Pattern.compile(Pattern.quote("No access rights"), Pattern.CASE_INSENSITIVE);
private static final Pattern ACCESS_DENIED_PATTERN_2 = private static final Pattern ACCESS_DENIED_PATTERN_2 =

View file

@ -117,7 +117,7 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
@Override @Override
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) { public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
if (log.isDebugEnabled()) { 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 return this.moodleCourseRestriction

View file

@ -128,14 +128,18 @@ class MoodleRestTemplateFactory {
.map(this::createRestTemplate) .map(this::createRestTemplate)
.map(result -> { .map(result -> {
if (result.hasError()) { 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; return result;
}) })
.filter(Result::hasValue) .filter(Result::hasValue)
.findFirst() .findFirst()
.orElse(Result.ofRuntimeError( .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) { Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) {
@ -146,7 +150,9 @@ class MoodleRestTemplateFactory {
final CharSequence accessToken = template.getAccessToken(); final CharSequence accessToken = template.getAccessToken();
if (accessToken == null) { 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; return template;

View file

@ -64,6 +64,7 @@ public interface ExamSessionService {
/** Use this to check the consistency of a running Exam. /** Use this to check the consistency of a running Exam.
* Current consistency checks are: * Current consistency checks are:
* - Check if there is at least one Exam supporter attached to the Exam * - 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 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 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 * - 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 * @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 * @return Result of one APIMessage per consistency check if the check failed. An empty Collection of everything is
* okay. */ * 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. /** Use this to check if a specified Exam has currently active SEB Client connections.
* *

View file

@ -133,7 +133,8 @@ public class ExamSessionCacheService {
case RUNNING: { case RUNNING: {
return true; return true;
} }
case UP_COMING: { case UP_COMING:
case FINISHED: {
return this.examUpdateHandler.updateRunning(exam.id) return this.examUpdateHandler.updateRunning(exam.id)
.map(e -> e.status == ExamStatus.RUNNING) .map(e -> e.status == ExamStatus.RUNNING)
.getOr(false); .getOr(false);

View file

@ -121,14 +121,22 @@ public class ExamSessionServiceImpl implements ExamSessionService {
} }
@Override @Override
public Result<Collection<APIMessage>> checkRunningExamConsistency(final Long examId) { public Result<Collection<APIMessage>> checkExamConsistency(final Long examId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final Collection<APIMessage> result = new ArrayList<>(); final Collection<APIMessage> result = new ArrayList<>();
if (isExamRunning(examId)) { final Exam exam = this.examDAO.byPK(examId)
final Exam exam = getRunningExam(examId)
.getOrThrow(); .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 // check exam supporter
if (exam.getSupporter().isEmpty()) { if (exam.getSupporter().isEmpty()) {
result.add(ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SUPPORTER.of(exam.getModelId())); result.add(ErrorMessage.EXAM_CONSISTENCY_VALIDATION_SUPPORTER.of(exam.getModelId()));

View file

@ -246,7 +246,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
checkReadPrivilege(institutionId); checkReadPrivilege(institutionId);
return this.examSessionService return this.examSessionService
.checkRunningExamConsistency(modelId) .checkExamConsistency(modelId)
.getOrThrow(); .getOrThrow();
} }

View file

@ -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-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-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.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.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 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.UP_COMING=Up Coming
sebserver.exam.status.RUNNING=Running sebserver.exam.status.RUNNING=Running
sebserver.exam.status.FINISHED=Finished 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.actions=
sebserver.exam.configuration.list.title=Exam Configuration sebserver.exam.configuration.list.title=Exam Configuration