From cb9900a16d175fabbba5a364e043c9f2144a0b00 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 14 Dec 2023 16:24:08 +0100 Subject: [PATCH] SEBSERV-457 finished implementation --- .../sebserver/gui/content/exam/ExamForm.java | 13 ++++---- .../sebserver/gui/form/TextFieldBuilder.java | 9 ++++++ .../gui/service/page/impl/PageAction.java | 2 +- .../sebserver/gui/widget/WidgetFactory.java | 5 +-- .../dao/impl/ConfigurationValueDAOImpl.java | 7 +++- .../servicelayer/dao/impl/ExamRecordDAO.java | 2 +- .../servicelayer/exam/ExamAdminService.java | 32 +++++++++++++------ .../exam/impl/ExamAdminServiceImpl.java | 10 ++++++ .../lms/impl/SEBRestrictionServiceImpl.java | 22 ++++++++++--- .../impl/mockup/MockSEBRestrictionAPI.java | 21 ++++++++++-- .../impl/ExamConfigUpdateServiceImpl.java | 4 +-- .../RemoteProctoringRoomServiceImpl.java | 10 ++++-- .../api/ExamAdministrationController.java | 8 +++-- 13 files changed, 109 insertions(+), 36 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java index a3259376..61435176 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java @@ -228,8 +228,7 @@ public class ExamForm implements TemplateComposer { final EntityGrantCheck entityGrantCheck = currentUser.entityGrantCheck(exam); final boolean modifyGrant = entityGrantCheck.m(); final boolean writeGrant = entityGrantCheck.w(); - final boolean editable = modifyGrant && - (exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.RUNNING); + final boolean editable = modifyGrant && (exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.RUNNING); final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean( exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); final boolean sebRestrictionAvailable = readonly && testSEBRestrictionAPI(exam); @@ -294,7 +293,8 @@ public class ExamForm implements TemplateComposer { final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(formContext .clearEntityKeys() - .removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA)); + .removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA) + .removeAttribute(AttributeKeys.NEW_EXAM_NO_LMS)); // propagate content actions to action-pane @@ -302,7 +302,8 @@ public class ExamForm implements TemplateComposer { .newAction(ActionDefinition.EXAM_MODIFY) .withEntityKey(entityKey) - .publishIf(() -> modifyGrant && readonly && editable) + .publishIf(() -> modifyGrant && readonly && + (editable || (exam.getStatus() == ExamStatus.FINISHED && exam.lmsSetupId == null))) .newAction(ActionDefinition.EXAM_DELETE) .withEntityKey(entityKey) @@ -316,7 +317,7 @@ public class ExamForm implements TemplateComposer { .publishIf(() -> writeGrant && readonly && exam.status == ExamStatus.FINISHED) .newAction(ActionDefinition.EXAM_SAVE) - .withExec(action -> (importFromQuizData) + .withExec(action -> importFromQuizData ? importExam(action, formHandle, sebRestrictionAvailable && exam.status == ExamStatus.RUNNING) : formHandle.processFormSave(action)) .ignoreMoveAwayFromEdit() @@ -493,7 +494,7 @@ public class ExamForm implements TemplateComposer { QuizData.QUIZ_ATTR_DESCRIPTION, FORM_DESCRIPTION_TEXT_KEY, exam.getDescription()) - .asHTML(50) + .asHTMLOrArea(50, exam.lmsSetupId != null) .readonly(true) .withInputSpan(7) .withEmptyCellSeparation(false)) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java index c269ae97..e7c9def1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/TextFieldBuilder.java @@ -94,6 +94,14 @@ public final class TextFieldBuilder extends FieldBuilder { return this; } + public TextFieldBuilder asHTMLOrArea(final int minHeight, final boolean html) { + if (html) { + return this.asHTML(minHeight); + } else { + return this.asArea(minHeight); + } + } + public TextFieldBuilder asMarkupLabel() { this.isMarkupLabel = true; return this; @@ -187,4 +195,5 @@ public final class TextFieldBuilder extends FieldBuilder { + HTML_TEXT_BLOCK_END; } + } \ No newline at end of file diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java index abf8c57b..9b8aef74 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java @@ -222,7 +222,7 @@ public final class PageAction { } catch (final FormPostException e) { if (e.getCause() instanceof RestCallError) { final RestCallError cause = (RestCallError) e.getCause(); - if (cause.isUnexpectedError()) { + if (cause.isUnexpectedError() || log.isDebugEnabled()) { log.error("Failed to execute action: {} | error: {} | cause: {}", PageAction.this.getName(), cause.getMessage(), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index 33c16aac..2cfedb57 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -535,8 +535,9 @@ public class WidgetFactory { final LocTextKey ariaLabel) { final Text input = readonly - ? new Text(content, SWT.LEFT | SWT.MULTI) - : new Text(content, SWT.LEFT | SWT.BORDER | SWT.MULTI); + ? new Text(content, SWT.LEFT | SWT.MULTI | SWT.WRAP) + : new Text(content, SWT.LEFT | SWT.BORDER | SWT.MULTI | SWT.WRAP); + if (ariaLabel != null) { WidgetFactory.setARIALabel(input, this.i18nSupport.getText(ariaLabel)); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationValueDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationValueDAOImpl.java index 62c76167..b03db180 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationValueDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ConfigurationValueDAOImpl.java @@ -28,6 +28,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.mybatis.dynamic.sql.SqlBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -147,7 +148,11 @@ public class ConfigurationValueDAOImpl implements ConfigurationValueDAO { attrId); } - return records.get(0).getValue(); + final String value = records.get(0).getValue(); + if (value == null) { + return StringUtils.EMPTY; + } + return value; }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java index 23682e39..e9101847 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java @@ -270,7 +270,7 @@ public class ExamRecordDAO { } if (exam.status != null && !exam.status.name().equals(oldRecord.getStatus())) { - log.warn("Exam state change on save. Exam. {}, Old state: {}, new state: {}", + log.info("Exam state change on save. Exam. {}, Old state: {}, new state: {}", exam.externalId, oldRecord.getStatus(), exam.status); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index 43d5834f..2ed1a012 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -67,7 +67,7 @@ public interface ExamAdminService { * @return Result refer to the created exam or to an error when happened */ Result applyAdditionalSEBRestrictions(Exam exam); - /** Indicates whether a specific exam is been restricted with SEB restriction feature on the LMS or not. + /** Indicates whether a specific exam is being restricted with SEB restriction feature on the LMS or not. * * @param exam The exam instance * @return Result refer to the restriction flag or to an error when happened */ @@ -164,14 +164,23 @@ public interface ExamAdminService { void notifyExamSaved(Exam exam); static void newExamFieldValidation(final POSTMapper postParams) { - final Collection validationErrors = new ArrayList<>(); + noLMSFieldValidation(new Exam(postParams)); + } - if (!postParams.contains(Domain.EXAM.ATTR_QUIZ_NAME)) { + static Exam noLMSFieldValidation(final Exam exam) { + + // This only applies to exams that has no LMS + if (exam.lmsSetupId != null) { + return exam; + } + + final Collection validationErrors = new ArrayList<>(); + if (StringUtils.isBlank(exam.name)) { validationErrors.add(APIMessage.fieldValidationError( Domain.EXAM.ATTR_QUIZ_NAME, "exam:quizName:notNull")); } else { - final int length = postParams.getString(Domain.EXAM.ATTR_QUIZ_NAME).length(); + final int length = exam.name.length(); if (length < 3 || length > 255) { validationErrors.add(APIMessage.fieldValidationError( Domain.EXAM.ATTR_QUIZ_NAME, @@ -179,13 +188,13 @@ public interface ExamAdminService { } } - if (!postParams.contains(QuizData.QUIZ_ATTR_START_URL)) { + if (StringUtils.isBlank(exam.getStartURL())) { validationErrors.add(APIMessage.fieldValidationError( QuizData.QUIZ_ATTR_START_URL, "exam:quiz_start_url:notNull")); } else { try { - new URL(postParams.getString(QuizData.QUIZ_ATTR_START_URL)).toURI(); + new URL(exam.getStartURL()).toURI(); } catch (final Exception e) { validationErrors.add(APIMessage.fieldValidationError( QuizData.QUIZ_ATTR_START_URL, @@ -193,13 +202,13 @@ public interface ExamAdminService { } } - if (!postParams.contains(Domain.EXAM.ATTR_QUIZ_START_TIME)) { + if (exam.startTime == null) { validationErrors.add(APIMessage.fieldValidationError( Domain.EXAM.ATTR_QUIZ_START_TIME, "exam:quizStartTime:notNull")); - } else if (postParams.contains(Domain.EXAM.ATTR_QUIZ_END_TIME)) { - if (postParams.getDateTime(Domain.EXAM.ATTR_QUIZ_START_TIME) - .isAfter(postParams.getDateTime(Domain.EXAM.ATTR_QUIZ_END_TIME))) { + } else if (exam.endTime != null) { + if (exam.startTime + .isAfter(exam.endTime)) { validationErrors.add(APIMessage.fieldValidationError( Domain.EXAM.ATTR_QUIZ_END_TIME, "exam:quizEndTime:endBeforeStart")); @@ -209,6 +218,8 @@ public interface ExamAdminService { if (!validationErrors.isEmpty()) { throw new APIMessageException(validationErrors); } + + return exam; } /** Used to check threshold consistency for a given list of thresholds. @@ -326,4 +337,5 @@ public interface ExamAdminService { } } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index 2ef74cea..99c90337 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -166,6 +166,11 @@ public class ExamAdminServiceImpl implements ExamAdminService { public Result applyAdditionalSEBRestrictions(final Exam exam) { return Result.tryCatch(() -> { + // this only applies to exams that are attached to an LMS + if (exam.lmsSetupId == null) { + return exam; + } + if (log.isDebugEnabled()) { log.debug("Apply additional SEB restrictions for exam: {}", exam.externalId); @@ -325,6 +330,11 @@ public class ExamAdminServiceImpl implements ExamAdminService { private Result initAdditionalAttributesForMoodleExams(final Exam exam) { return Result.tryCatch(() -> { + + if (exam.lmsSetupId == null) { + return exam; + } + final LmsAPITemplate lmsTemplate = this.lmsAPIService .getLmsAPITemplate(exam.lmsSetupId) .getOrThrow(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java index b6d841cf..68a6c106 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java @@ -84,9 +84,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { // check only if SEB_RESTRICTION feature is on if (lmsSetup != null && lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) { - if (!exam.sebRestriction) { - return false; - } + return exam.sebRestriction; } return true; @@ -97,12 +95,11 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { public Result getSEBRestrictionFromExam(final Exam exam) { return Result.tryCatch(() -> { // load the config keys from restriction and merge with new generated config keys - final Set configKeys = new HashSet<>(); final Collection generatedKeys = this.examConfigService .generateConfigKeys(exam.institutionId, exam.id) .getOrThrow(); - configKeys.addAll(generatedKeys); + final Set configKeys = new HashSet<>(generatedKeys); if (!generatedKeys.isEmpty()) { configKeys.addAll(this.lmsAPIService .getLmsAPITemplate(exam.lmsSetupId) @@ -210,6 +207,11 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { @EventListener(ExamStartedEvent.class) public void notifyExamStarted(final ExamStartedEvent event) { + // This affects only exams with LMS binding... + if (event.exam.lmsSetupId == null) { + return; + } + if (log.isDebugEnabled()) { log.debug("ExamStartedEvent received, process applySEBClientRestriction..."); } @@ -225,6 +227,11 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { @EventListener(ExamFinishedEvent.class) public void notifyExamFinished(final ExamFinishedEvent event) { + // This affects only exams with LMS binding... + if (event.exam.lmsSetupId == null) { + return; + } + if (log.isDebugEnabled()) { log.debug("ExamFinishedEvent received, process releaseSEBClientRestriction..."); } @@ -260,6 +267,11 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { @Override public Result applySEBClientRestriction(final Exam exam) { return Result.tryCatch(() -> { + if (exam.lmsSetupId == null) { + log.info("No LMS for Exam: {}", exam.name); + return exam; + } + if (!this.lmsAPIService .getLmsSetup(exam.lmsSetupId) .getOrThrow().lmsType.features.contains(Features.SEB_RESTRICTION)) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java index b8d1f097..3d5aca2c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java @@ -27,7 +27,7 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI { private static final Logger log = LoggerFactory.getLogger(MockSEBRestrictionAPI.class); - //private Map restrictionDB = new ConcurrentHashMap<>(); + private Map restrictionDB = new ConcurrentHashMap<>(); @Override public LmsSetupTestResult testCourseRestrictionAPI() { @@ -37,7 +37,11 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI { @Override public Result getSEBClientRestriction(final Exam exam) { log.info("Get SEB Client restriction for Exam: {}", exam); - return Result.ofError(new NoSEBRestrictionException()); + if (!restrictionDB.containsKey(exam.id)) { + return Result.ofError(new NoSEBRestrictionException()); + } else { + return Result.of(restrictionDB.get(exam.id)); + } } @Override @@ -46,13 +50,24 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI { final SEBRestriction sebRestrictionData) { log.info("Apply SEB Client restriction: {}", sebRestrictionData); - //return Result.ofError(new NoSEBRestrictionException()); + restrictionDB.put(exam.id, sebRestrictionData); return Result.of(sebRestrictionData); } @Override public Result releaseSEBClientRestriction(final Exam exam) { log.info("Release SEB Client restriction for Exam: {}", exam); + if (restrictionDB.containsKey(exam.id)) { + SEBRestriction sebRestriction = restrictionDB.get(exam.id); + restrictionDB.put( + exam.id, + new SEBRestriction( + exam.id, + null, + null, + sebRestriction.additionalProperties, + sebRestriction.warningMessage)); + } return Result.of(exam); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java index 7a4a16df..b3ff107f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java @@ -106,7 +106,7 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService { log.debug("Update-Lock successfully placed for all involved exams: {}", examsIds); } - // check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime + // check running exam integrity again after lock to ensure there were no SEB Client connection attempts in the meantime final Collection examIdsSecondCheck = checkRunningExamIntegrity(configurationNodeId) .getOrThrow(); @@ -129,7 +129,7 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService { // generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key) for (final Exam exam : exams) { - if (exam.getStatus() == ExamStatus.RUNNING) { + if (exam.getStatus() == ExamStatus.RUNNING && exam.lmsSetupId != null) { this.examUpdateHandler .getSEBRestrictionService() diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/RemoteProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/RemoteProctoringRoomServiceImpl.java index 387d6433..853d357f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/RemoteProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/RemoteProctoringRoomServiceImpl.java @@ -172,7 +172,9 @@ public class RemoteProctoringRoomServiceImpl implements RemoteProctoringRoomServ @EventListener public void notifyExamFinished(final ExamFinishedEvent event) { - log.info("ExamFinishedEvent received, process disposeRoomsForExam..."); + if (log.isDebugEnabled()) { + log.debug("ExamFinishedEvent received, process disposeRoomsForExam..."); + } disposeRoomsForExam(event.exam) .onError(error -> log.error("Failed to dispose rooms for finished exam: {}", event.exam, error)); @@ -183,12 +185,14 @@ public class RemoteProctoringRoomServiceImpl implements RemoteProctoringRoomServ return Result.tryCatch(() -> { - log.info("Dispose and deleting proctoring rooms for exam: {}", exam.externalId); - final ProctoringServiceSettings proctoringSettings = this.examAdminService .getProctoringServiceSettings(exam.id) .getOrThrow(); + if (proctoringSettings.enableProctoring) { + log.info("Dispose and deleting proctoring rooms for exam: {}", exam.externalId); + } + this.proctoringAdminService .getExamProctoringService(proctoringSettings.serverType) .flatMap(service -> service.disposeServiceRoomsForExam(exam.id, proctoringSettings)) 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 45b004fa..e017a300 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 @@ -539,7 +539,7 @@ public class ExamAdministrationController extends EntityController { + API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) - public ScreenProctoringSettings getScreenProctoringeSettings( + public ScreenProctoringSettings getScreenProctoringSettings( @RequestParam( name = API.PARAM_INSTITUTION_ID, required = true, @@ -655,6 +655,9 @@ public class ExamAdministrationController extends EntityController { errors.add(0, ErrorMessage.EXAM_IMPORT_ERROR_AUTO_SETUP.of( entity.getModelId(), API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + entity.getModelId())); + + log.warn("Exam successfully created but some initialization did go wrong: {}", errors); + throw new APIMessageException(errors); } else { return this.examDAO.byPK(entity.id); @@ -679,7 +682,8 @@ public class ExamAdministrationController extends EntityController { @Override protected Result validForSave(final Exam entity) { return super.validForSave(entity) - .map(this::checkExamSupporterRole); + .map(this::checkExamSupporterRole) + .map(ExamAdminService::noLMSFieldValidation); } @Override