diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java index a9609191..6c6e0d1b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java @@ -48,13 +48,16 @@ public class APIMessage implements Serializable { PASSWORD_MISMATCH("1300", HttpStatus.BAD_REQUEST, "new password do not match confirmed password"), MISSING_PASSWORD("1301", HttpStatus.BAD_REQUEST, "Missing Password"), + BINDING_ERROR("1500", HttpStatus.BAD_REQUEST, "External binding error"), + EXAM_CONSISTENCY_VALIDATION_SUPPORTER("1400", HttpStatus.OK, "No Exam Supporter 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, "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"), - - BINDING_ERROR("1500", HttpStatus.BAD_REQUEST, "External binding error"); + 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; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 193cbab1..d9a73d4c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -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 { diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java index fe631d8b..f2a661f8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java @@ -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(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java index 644d97af..d7494bd6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java @@ -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.ProctoringServiceSettings; 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 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 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; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamFormConfigs.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamFormConfigs.java index b7c20c20..a8e54d38 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamFormConfigs.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamFormConfigs.java @@ -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( diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamFormIndicators.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamFormIndicators.java index ece120cf..6c0b858a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamFormIndicators.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamFormIndicators.java @@ -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); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java index e7be7a46..4b066b8c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamList.java @@ -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; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamSEBRestrictionSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamSEBRestrictionSettings.java index ac63677c..78e99f4e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamSEBRestrictionSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamSEBRestrictionSettings.java @@ -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 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; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/ViewContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/ViewContext.java index 20e1fabf..bce56121 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/ViewContext.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/ViewContext.java @@ -64,6 +64,10 @@ public final class ViewContext { this.readonly = readonly; } + public boolean isReadonly() { + return this.readonly; + } + public I18nSupport getI18nSupport() { return this.i18nSupport; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/rules/BrowserViewModeRule.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/rules/BrowserViewModeRule.java index 8d7686f5..16328215 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/rules/BrowserViewModeRule.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/rules/BrowserViewModeRule.java @@ -45,7 +45,7 @@ public class BrowserViewModeRule implements ValueChangeRule { final ConfigurationAttribute attribute, final ConfigurationValue value) { - if (StringUtils.isBlank(value.value)) { + if (context.isReadonly() || StringUtils.isBlank(value.value)) { return; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/rules/IgnoreSEBService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/rules/IgnoreSEBService.java index e28f0b8e..e5f8e7b5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/rules/IgnoreSEBService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/examconfig/impl/rules/IgnoreSEBService.java @@ -41,6 +41,10 @@ public class IgnoreSEBService implements ValueChangeRule { final ConfigurationAttribute attribute, final ConfigurationValue value) { + if (context.isReadonly()) { + return; + } + if (KEY_IGNORE_SEB_SERVICE.equals(attribute.name)) { if (BooleanUtils.toBoolean(value.value)) { context.disable(KEY_SEB_SERVICE_POLICY); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index b92f3dbf..9f9f355c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -102,7 +102,7 @@ public class ExamDAOImpl implements ExamDAO { @Transactional(readOnly = true) public Result 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 @@ -431,7 +431,7 @@ public class ExamDAOImpl implements ExamDAO { isEqualTo(BooleanUtils.toInteger(true))) .and( ExamRecordDynamicSqlSupport.status, - isEqualTo(ExamStatus.UP_COMING.name())) + isNotEqualTo(ExamStatus.RUNNING.name())) .and( ExamRecordDynamicSqlSupport.updating, isEqualTo(BooleanUtils.toInteger(false))) @@ -793,17 +793,48 @@ public class ExamDAOImpl implements ExamDAO { final Map 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> getQuizzesFromLMS(final LmsAPITemplate template, final Set ids, + private Collection> getQuizzesFromLMS( + final LmsAPITemplate template, + final Set 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 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()); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java index 9b2d7298..2d192400 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleCourseAccess.java @@ -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> quizzesSupplier(final Set ids) { return () -> getRestTemplate() .map(template -> getQuizzesForIds(template, ids)) - .getOrThrow(); + .getOr(Collections.emptyList()); } protected Supplier> 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 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 = diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java index 8cd903bc..e37ad8a1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleLmsAPITemplate.java @@ -117,7 +117,7 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate { @Override public Result 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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java index 13cbd60f..605bc37b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java @@ -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 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; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java index 6f60a330..9e77af50 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigIO.java @@ -37,6 +37,7 @@ import org.xml.sax.SAXException; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -140,7 +141,7 @@ public class ExamConfigIO { .collect(Collectors.toList()); final Function configurationValueSupplier = - getConfigurationValueSupplier(institutionId, configurationId); + getConfigurationValueSupplier(configurationId); writeHeader(exportFormat, out); @@ -316,11 +317,13 @@ public class ExamConfigIO { } private Function getConfigurationValueSupplier( - final Long institutionId, final Long configurationId) { + final Configuration configuration = this.configurationDAO.byPK(configurationId) + .getOrThrow(); + final Map mapping = this.configurationValueDAO - .allRootAttributeValues(institutionId, configurationId) + .allRootAttributeValues(configuration.institutionId, configurationId) .getOrThrow() .stream() .collect(Collectors.toMap( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigServiceImpl.java index 144de3f8..46870da6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ExamConfigServiceImpl.java @@ -267,26 +267,6 @@ public class ExamConfigServiceImpl implements ExamConfigService { log.debug("Start to stream plain JSON SEB Configuration data for Config-Key generation"); } -// if (true) { -// PipedOutputStream pout; -// PipedInputStream pin; -// try { -// pout = new PipedOutputStream(); -// pin = new PipedInputStream(pout); -// this.examConfigIO.exportPlain( -// ConfigurationFormat.JSON, -// pout, -// institutionId, -// configurationNodeId); -// -// final String json = IOUtils.toString(pin, "UTF-8"); -// -// log.trace("SEB Configuration JSON to create Config-Key: {}", json); -// } catch (final Exception e) { -// log.error("Failed to trace SEB Configuration JSON: ", e); -// } -// } - PipedOutputStream pout = null; PipedInputStream pin = null; try { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/TableConverter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/TableConverter.java index e1a74c06..96c3ade4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/TableConverter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/converter/TableConverter.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.converter; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -109,11 +110,17 @@ public class TableConverter implements AttributeValueConverter { final ConfigurationValue value, final boolean xml) throws IOException { - final List> values = this.configurationValueDAO.getOrderedTableValues( - value.institutionId, - value.configurationId, - attribute.id) - .getOrThrow(); + final List> values = new ArrayList<>(); + if (value != null) { + values.addAll(this.configurationValueDAO.getOrderedTableValues( + value.institutionId, + value.configurationId, + attribute.id) + .onError(error -> log.error("Failed to get table values for attribute: {}", attribute.name, error)) + .getOrElse(() -> Collections.emptyList())); + } else { + log.warn("No ConfigurationValue for table: {}. Convert to empty table", attribute); + } final boolean noValues = CollectionUtils.isEmpty(values); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java index a1a68ece..c17bea1c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java @@ -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> checkRunningExamConsistency(Long examId); + Result> checkExamConsistency(Long examId); /** Use this to check if a specified Exam has currently active SEB Client connections. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java index 67a531f1..b26c2434 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java @@ -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); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index 248c8bee..d519a294 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -121,14 +121,22 @@ public class ExamSessionServiceImpl implements ExamSessionService { } @Override - public Result> checkRunningExamConsistency(final Long examId) { + public Result> checkExamConsistency(final Long examId) { return Result.tryCatch(() -> { final Collection 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())); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java index c6568112..c83b9d60 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java @@ -45,7 +45,7 @@ class ExamUpdateHandler { this.examDAO = examDAO; this.sebRestrictionService = sebRestrictionService; - this.updatePrefix = webserviceInfo.getHostAddress() + this.updatePrefix = webserviceInfo.getLocalHostAddress() + "_" + webserviceInfo.getServerPort() + "_"; this.examTimeSuffix = examTimeSuffix; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 86fdb58c..214a0c2e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -246,7 +246,7 @@ public class ExamAdministrationController extends EntityController { checkReadPrivilege(institutionId); return this.examSessionService - .checkRunningExamConsistency(modelId) + .checkExamConsistency(modelId) .getOrThrow(); } diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties index 5ec2e5a1..7a2939a8 100644 --- a/src/main/resources/config/application-dev.properties +++ b/src/main/resources/config/application-dev.properties @@ -8,6 +8,7 @@ server.tomcat.uri-encoding=UTF-8 logging.level.ch=INFO logging.level.org.springframework.cache=INFO logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG sebserver.http.client.connect-timeout=150000 sebserver.http.client.connection-request-timeout=100000 diff --git a/src/main/resources/config/sql/base/V5_2__insert_new_security_settings_v.1.1.sql b/src/main/resources/config/sql/base/V5_2__insert_new_security_settings_v.1.1.sql index 18207a50..fea8ad0e 100644 --- a/src/main/resources/config/sql/base/V5_2__insert_new_security_settings_v.1.1.sql +++ b/src/main/resources/config/sql/base/V5_2__insert_new_security_settings_v.1.1.sql @@ -8,8 +8,8 @@ INSERT IGNORE INTO configuration_attribute VALUES UPDATE orientation SET y_position='13', width='12' WHERE id='305'; -UPDATE orientation SET y_position='16', width='10' WHERE id='306'; -UPDATE orientation SET y_position='17', width='10' WHERE id='307'; +UPDATE orientation SET y_position='16', width='9' WHERE id='306'; +UPDATE orientation SET y_position='17', width='9' WHERE id='307'; UPDATE orientation SET y_position='18' WHERE id='317'; UPDATE orientation SET x_position='0', y_position='9' WHERE id='301'; UPDATE orientation SET x_position='3', y_position='10', width='4' WHERE id='501'; diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 33f59347..4decbf93 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -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.
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.
Please check the LMS connection within the LMS Setup. +sebserver.exam.consistency.invalid-lms-id= - The referencing course identifier seems to be invalid.
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
where connecting SEB clients cannot download the configuration for the exam.

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