From c4dc211cb6eb342561ddb9002fe51dd4e66886f4 Mon Sep 17 00:00:00 2001 From: anhefti Date: Tue, 11 Jun 2024 13:13:02 +0200 Subject: [PATCH] SEBSERV-417 improved error and waning handling and logging --- .../seb/sebserver/gbl/model/Activatable.java | 6 ++ .../gui/content/exam/LmsSetupForm.java | 5 ++ .../gui/content/exam/LmsSetupList.java | 1 + .../gui/service/page/PageContext.java | 1 + .../service/page/impl/PageServiceImpl.java | 8 +- .../dao/impl/LmsSetupDAOImpl.java | 3 - .../lms/FullLmsIntegrationService.java | 3 + .../lms/SEBRestrictionService.java | 8 ++ .../impl/FullLmsIntegrationServiceImpl.java | 90 +++++++++++++------ .../lms/impl/LmsAPITemplateAdapter.java | 22 ++--- .../lms/impl/LmsSetupChangeEvent.java | 6 +- .../lms/impl/LmsTestServiceImpl.java | 67 ++++++++------ .../lms/impl/SEBRestrictionServiceImpl.java | 48 ++++++++++ .../moodle/MoodleRestTemplateFactoryImpl.java | 12 ++- .../plugin/MoodlePluginFullIntegration.java | 16 ++-- .../servicelayer/session/ExamUpdateTask.java | 4 + .../api/ActivatableEntityController.java | 7 +- .../weblayer/api/EntityController.java | 4 + .../weblayer/api/LmsSetupController.java | 53 ++++++++++- src/main/resources/messages.properties | 4 + .../seb/sebserver/gbl/JSONParserTest.java | 60 +++++++++++++ 21 files changed, 344 insertions(+), 84 deletions(-) create mode 100644 src/test/java/ch/ethz/seb/sebserver/gbl/JSONParserTest.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/Activatable.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/Activatable.java index c5f00bab..34c592bd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/Activatable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/Activatable.java @@ -12,6 +12,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; public interface Activatable { + public enum ActivationAction { + NONE, + ACTIVATE, + DEACTIVATE + } + @JsonIgnore boolean isActive(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/LmsSetupForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/LmsSetupForm.java index b34bfb8d..59c30d96 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/LmsSetupForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/LmsSetupForm.java @@ -514,6 +514,11 @@ public class LmsSetupForm implements TemplateComposer { Utils.escapeHTML_XML_EcmaScript(error.message))); return onOK.apply(locTextKey); } + case APPLY_FULL_INTEGRATION: { + throw new PageMessageException(new LocTextKey( + "sebserver.lmssetup.action.test.fullintegration.error", + Utils.formatHTMLLinesForceEscaped(Utils.escapeHTML_XML_EcmaScript(error.message)))); + } default: { throw new PageMessageException(new LocTextKey( "sebserver.lmssetup.action.test.unknownError", diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/LmsSetupList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/LmsSetupList.java index b43a3c13..92e085ed 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/LmsSetupList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/LmsSetupList.java @@ -192,6 +192,7 @@ public class LmsSetupList implements TemplateComposer { .publishIf(userGrant::im, false) .newAction(ActionDefinition.LMS_SETUP_TOGGLE_ACTIVITY) + .withAttribute(PageContext.CONTEXTUAL_ERROR_KEY, "sebserver.lmssetup.action.activation.error") .withSelect( table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION), this.pageService.activationToggleActionFunction( diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java index 1aca9da7..efbf4541 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java @@ -52,6 +52,7 @@ public interface PageContext { } + String CONTEXTUAL_ERROR_KEY = "ERROR_MESSAGE_KEY"; /** The resource-bundle key of the generic load entity error message. */ String GENERIC_LOAD_ERROR_TEXT_KEY = "sebserver.error.get.entity"; String GENERIC_REMOVE_ERROR_TEXT_KEY = "sebserver.error.remove.entity"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java index 2499be1c..9406872c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java @@ -326,9 +326,15 @@ public class PageServiceImpl implements PageService { if (!errors.isEmpty()) { final String entityTypeName = this.resourceService.getEntityTypeName(entityType); + final String errorMessageKey = action.pageContext().getAttribute(PageContext.CONTEXTUAL_ERROR_KEY); throw new MultiPageMessageException( - new LocTextKey(PageContext.GENERIC_ACTIVATE_ERROR_TEXT_KEY, entityTypeName), + new LocTextKey( + (errorMessageKey != null) + ? errorMessageKey + : PageContext.GENERIC_ACTIVATE_ERROR_TEXT_KEY, + entityTypeName), errors); + } return action; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java index a81d3b87..1e67f28f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java @@ -144,9 +144,6 @@ public class LmsSetupDAOImpl implements LmsSetupDAO { return this.lmsSetupRecordMapper.selectIdsByExample() .where( - LmsSetupRecordDynamicSqlSupport.active, - isEqualTo(1)) - .and( lmsType, isIn(types)) .build() diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java index 4a16abed..519c5a14 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java @@ -14,6 +14,7 @@ import java.util.Collection; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent; @@ -31,6 +32,8 @@ public interface FullLmsIntegrationService { @EventListener void notifyLmsSetupChange(final LmsSetupChangeEvent event); + Result applyLMSSetupDeactivation(LmsSetup lmsSetup); + @EventListener void notifyExamTemplateChange(final ExamTemplateChangeEvent event); @EventListener(ConnectionConfigurationChangeEvent.class) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java index 385e47c3..656dc290 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java @@ -10,7 +10,10 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsSetupChangeEvent; +import org.springframework.context.event.EventListener; public interface SEBRestrictionService { @@ -67,4 +70,9 @@ public interface SEBRestrictionService { Result applyQuitPassword(final Exam exam); + @EventListener + void notifyLmsSetupChange(final LmsSetupChangeEvent event); + + Result applyLMSSetupDeactivation(LmsSetup lmsSetup); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java index 81d7c140..1b935d17 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java @@ -24,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; +import ch.ethz.seb.sebserver.gbl.model.Activatable; 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; @@ -137,7 +138,6 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService .getClientHttpRequestFactory() .onSuccess(this.restTemplate::setRequestFactory) .onError(error -> log.warn("Failed to set HTTP request factory: ", error)); - //this.restTemplate.setErrorHandler(new OAuth2AuthorizationContextHolder.OAuth2AuthorizationContext.ErrorHandler(this.resource)); this.restTemplate .getMessageConverters() .add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8)); @@ -164,19 +164,46 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService return; } - if (lmsSetup.active) { + if (event.activation == Activatable.ActivationAction.NONE) { if (!lmsSetup.integrationActive) { applyFullLmsIntegration(lmsSetup.id) - .onError(error -> log.warn("Failed to update LMS integration for: {}", lmsSetup, error)) + .onError(error -> log.warn("Failed to update LMS integration for: {} error {}", lmsSetup, error.getMessage())) .onSuccess(data -> log.debug("Successfully updated LMS integration for: {} data: {}", lmsSetup, data)); } - } else if (lmsSetup.integrationActive) { - deleteFullLmsIntegration(lmsSetup.id) - .onError(error -> log.warn("Failed to delete LMS integration for: {}", lmsSetup, error)) - .onSuccess(data -> log.debug("Successfully deleted LMS integration for: {} data: {}", lmsSetup, data)); + } else if (event.activation == Activatable.ActivationAction.ACTIVATE) { + applyFullLmsIntegration(lmsSetup.id) + .map(data -> reapplyExistingExams(data,lmsSetup)) + .onError(error -> log.warn("Failed to update LMS integration for: {} error {}", lmsSetup, error.getMessage())) + .onSuccess(data -> log.debug("Successfully updated LMS integration for: {} data: {}", lmsSetup, data)); } } + @Override + public Result applyLMSSetupDeactivation(final LmsSetup lmsSetup) { + if (!lmsSetup.getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { + return Result.of(lmsSetup); + } + + return Result.tryCatch(() -> { + + // remove all active exam data for involved exams before deactivate them + this.examDAO + .allActiveForLMSSetup(Arrays.asList(lmsSetup.id)) + .getOrThrow() + .forEach( exam -> { + this.teacherAccountServiceImpl.deactivateTeacherAccountsForExam(exam) + .map(e -> applyExamData(e, true)) + .onError(error -> log.warn("Failed delete teacher accounts for exam: {}", exam.name)); + }); + + // delete full integration on Moodle side before deactivate LMS Setup + this.deleteFullLmsIntegration(lmsSetup.id) + .getOrThrow(); + + return lmsSetup; + }); + } + @Override public void notifyExamTemplateChange(final ExamTemplateChangeEvent event) { final ExamTemplate examTemplate = event.getExamTemplate(); @@ -257,8 +284,6 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService }); } - - @Override public Result deleteFullLmsIntegration(final Long lmsSetupId) { return lmsSetupDAO @@ -327,28 +352,24 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService try { - // TODO this is hardcoded for Testing, below out-commented code is real business + final Result examResult = lmsSetupDAO + .getLmsSetupIdByConnectionId(lmsUUID) + .flatMap(lmsAPITemplateCacheService::getLmsAPITemplate) + .map(findQuizData(courseId, quizId)) + .flatMap(this::findExam); - this.connectionConfigurationService.exportSEBClientConfiguration(out, "1", null); + if (examResult.hasError()) { + throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("Exam not found")); + } -// final Result examResult = lmsSetupDAO -// .getLmsSetupIdByConnectionId(lmsUUID) -// .flatMap(lmsAPIService::getLmsAPITemplate) -// .map(findQuizData(courseId, quizId)) -// .flatMap(this::findExam); -// -// if (examResult.hasError()) { -// throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("Exam not found")); -// } -// -// final Exam exam = examResult.get(); -// -// final String connectionConfigId = getConnectionConfigurationId(exam); -// if (StringUtils.isBlank(connectionConfigId)) { -// throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("No active Connection Configuration found")); -// } -// -// this.connectionConfigurationService.exportSEBClientConfiguration(out, connectionConfigId, exam.id); + final Exam exam = examResult.get(); + + final String connectionConfigId = getConnectionConfigurationId(exam); + if (StringUtils.isBlank(connectionConfigId)) { + throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("No active Connection Configuration found")); + } + + this.connectionConfigurationService.exportSEBClientConfiguration(out, connectionConfigId, exam.id); return Result.EMPTY; } catch (final Exception e) { @@ -356,6 +377,17 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService } } + private LmsSetup reapplyExistingExams( + final IntegrationData integrationData, + final LmsSetup lmsSetup) { + + examDAO.allActiveForLMSSetup(Arrays.asList(lmsSetup.id)) + .getOrThrow() + .forEach(exam -> applyExamData(exam, false)); + + return lmsSetup; + } + private String getConnectionConfigurationId(final Exam exam) { String connectionConfigId = exam.getAdditionalAttribute(Exam.ADDITIONAL_ATTR_DEFAULT_CONNECTION_CONFIGURATION); if (StringUtils.isBlank(connectionConfigId)) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java index 4ca3577a..6cd54f8e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java @@ -36,6 +36,8 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { private static final Logger log = LoggerFactory.getLogger(LmsAPITemplateAdapter.class); + private static final int DEFAULT_ATTEMPTS = 1; + private final CourseAccessAPI courseAccessAPI; private final SEBRestrictionAPI sebRestrictionAPI; @@ -78,7 +80,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.lmsTestRequest.attempts", Integer.class, - 2), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.lmsTestRequest.blockingTime", Long.class, @@ -92,7 +94,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.lmsAccessRequest.attempts", Integer.class, - 2), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.lmsAccessRequest.blockingTime", Long.class, @@ -106,7 +108,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.applyExamDataRequest.attempts", Integer.class, - 2), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.applyExamDataRequest.blockingTime", Long.class, @@ -120,7 +122,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.lmsTestRequest.attempts", Integer.class, - 2), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.lmsTestRequest.blockingTime", Long.class, @@ -134,7 +136,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.quizzesRequest.attempts", Integer.class, - 1), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime", Long.class, @@ -148,7 +150,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.quizzesRequest.attempts", Integer.class, - 1), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime", Long.class, @@ -162,7 +164,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.quizzesRequest.attempts", Integer.class, - 1), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.quizzesRequest.blockingTime", Long.class, @@ -176,7 +178,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.chaptersRequest.attempts", Integer.class, - 1), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.chaptersRequest.blockingTime", Long.class, @@ -204,7 +206,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.sebrestriction.attempts", Integer.class, - 1), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.sebrestriction.blockingTime", Long.class, @@ -218,7 +220,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { environment.getProperty( "sebserver.webservice.circuitbreaker.examRequest.attempts", Integer.class, - 2), + DEFAULT_ATTEMPTS), environment.getProperty( "sebserver.webservice.circuitbreaker.examRequest.blockingTime", Long.class, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java index 81b6fc31..cbd8210e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsSetupChangeEvent.java @@ -8,6 +8,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; +import ch.ethz.seb.sebserver.gbl.model.Activatable; import org.springframework.context.ApplicationEvent; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; @@ -16,8 +17,11 @@ public class LmsSetupChangeEvent extends ApplicationEvent { private static final long serialVersionUID = -7239994198026689531L; - public LmsSetupChangeEvent(final LmsSetup source) { + public final Activatable.ActivationAction activation; + + public LmsSetupChangeEvent(final LmsSetup source, final Activatable.ActivationAction activation) { super(source); + this.activation = activation; } public LmsSetup getLmsSetup() { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsTestServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsTestServiceImpl.java index 5fe1497d..62356884 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsTestServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsTestServiceImpl.java @@ -17,6 +17,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationServi import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateCacheService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsTestService; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleResponseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; @@ -58,27 +59,8 @@ public class LmsTestServiceImpl implements LmsTestService { } } - if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { - final Long lmsSetupId = template.lmsSetup().id; - final LmsSetupTestResult lmsSetupTestResult = template.testFullIntegrationAPI(); - if (!lmsSetupTestResult.isOk()) { - lmsAPITemplateCacheService.clearCache(template.lmsSetup().getModelId()); - this.lmsSetupDAO - .setIntegrationActive(lmsSetupId, false) - .onError(er -> log.error("Failed to mark LMS integration inactive", er)); - return lmsSetupTestResult; - } else { - - final Result integrationDataResult = fullLmsIntegrationService - .applyFullLmsIntegration(template.lmsSetup().id); - - if (integrationDataResult.hasError()) { - return LmsSetupTestResult.ofFullIntegrationAPIError( - template.lmsSetup().lmsType, - "Failed to apply full LMS integration"); - } - } - } + final LmsSetupTestResult lmsSetupTestResult = fullIntegrationTest(template); + if (lmsSetupTestResult != null) return lmsSetupTestResult; return LmsSetupTestResult.ofOkay(template.lmsSetup().getLmsType()); } @@ -110,13 +92,44 @@ public class LmsTestServiceImpl implements LmsTestService { } } - if (lmsType.features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { - final LmsSetupTestResult lmsSetupTestResult = lmsSetupTemplate.testFullIntegrationAPI(); - if (!lmsSetupTestResult.isOk()) { - return lmsSetupTestResult; - } - } + final LmsSetupTestResult lmsSetupTestResult = fullIntegrationTest(lmsSetupTemplate); + if (lmsSetupTestResult != null) return lmsSetupTestResult; return LmsSetupTestResult.ofOkay(lmsSetupTemplate.lmsSetup().getLmsType()); } + + private LmsSetupTestResult fullIntegrationTest(final LmsAPITemplate template) { + if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { + final Long lmsSetupId = template.lmsSetup().id; + final LmsSetupTestResult lmsSetupTestResult = template.testFullIntegrationAPI(); + if (!lmsSetupTestResult.isOk()) { + lmsAPITemplateCacheService.clearCache(template.lmsSetup().getModelId()); + this.lmsSetupDAO + .setIntegrationActive(lmsSetupId, false) + .onError(er -> log.error("Failed to mark LMS integration inactive", er)); + return lmsSetupTestResult; + } else { + + final Result integrationDataResult = fullLmsIntegrationService + .applyFullLmsIntegration(template.lmsSetup().id); + + if (integrationDataResult.hasError()) { + Throwable error = integrationDataResult.getError(); + if (error instanceof RuntimeException) { + error = error.getCause(); + } + if (error != null && error instanceof MoodleResponseException) { + return LmsSetupTestResult.ofFullIntegrationAPIError( + template.lmsSetup().lmsType, + error.getMessage()); + } else { + return LmsSetupTestResult.ofFullIntegrationAPIError( + template.lmsSetup().lmsType, + "Failed to apply full LMS integration"); + } + } + } + } + return null; + } } 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 770e27ef..aed220ae 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 @@ -18,6 +18,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; import org.apache.commons.lang3.StringUtils; @@ -106,6 +107,53 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { .onError(t -> log.error("Failed to quit password for Exam: {}", exam, t)); } + @Override + public void notifyLmsSetupChange(final LmsSetupChangeEvent event) { + final LmsSetup lmsSetup = event.getLmsSetup(); + // only relevant for LMS Setups with SEB restriction feature + if (!lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) { + return; + } + try { + if (event.activation == Activatable.ActivationAction.ACTIVATE) { + examDAO.allActiveForLMSSetup(Arrays.asList(lmsSetup.id)) + .getOrThrow() + .forEach(exam -> { + try { + this.applySEBRestrictionIfExamRunning(exam); + } catch (final Exception e) { + log.warn("Failed to update SEB restriction for exam: {} error: {}", exam.name, e.getMessage()); + } + }); + } + } catch (final Exception e) { + log.error("Failed to update SEB restriction for re-activated exams: {}", e.getMessage()); + } + } + + @Override + public Result applyLMSSetupDeactivation(final LmsSetup lmsSetup) { + + return Result.tryCatch(() -> { + // only relevant for LMS Setups with SEB restriction feature + if (!lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) { + return lmsSetup; + } + + examDAO.allActiveForLMSSetup(Arrays.asList(lmsSetup.id)) + .getOrThrow() + .forEach( exam -> { + this.releaseSEBClientRestriction(exam) + .onError(error -> log.warn( + "Failed to release SEB Restriction for Exam: {} error: {}", + exam.name, + error.getMessage())); + }); + + return lmsSetup; + }); + } + private Exam applySEBRestrictionIfExamRunning(final Exam exam) { if (exam.status != Exam.ExamStatus.RUNNING) { return exam; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java index 5608d489..8064fc08 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java @@ -9,6 +9,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle; import javax.net.ssl.SSLContext; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; @@ -310,8 +312,6 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory return callMoodleAPIFunction(functionName, null, null); } - - @Override public String callMoodleAPIFunction( final String functionName, @@ -338,6 +338,14 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory final String body = createMoodleFormPostBody(queryAttributes); + // TODO remove this after testing + try { + final String uriString = URLDecoder.decode(queryParam.toUriString(), "UTF8"); + log.info("POST To Moodle URI (decoded UTF8): {}, body: {}", uriString, body); + } catch (final Exception e) { + // ignore + } + final HttpHeaders headers = new HttpHeaders(); headers.set( HttpHeaders.CONTENT_TYPE, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java index 431ab754..1b3f47e1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java @@ -105,9 +105,9 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI { final String connectionJSON = jsonMapper.writeValueAsString(data); final MoodleAPIRestTemplate rest = getRestTemplate().getOrThrow(); - //if (log.isDebugEnabled()) { - log.info("Try to connect to Moodle Plugin 2.0 with: {}", connectionJSON); - //} + if (log.isDebugEnabled()) { + log.debug("Try to connect to Moodle Plugin 2.0 with: {}", connectionJSON); + } final MultiValueMap queryAttributes = new LinkedMultiValueMap<>(); queryAttributes.add(ATTRIBUTE_CONNECTION, connectionJSON); @@ -141,17 +141,19 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI { fullConnectionApplyResponse.warnings.stream() .filter(w -> Objects.equals(w.warningcode, "connectiondoesntmatch")) .findFirst() - .ifPresent( w -> { - - throw new MoodleResponseException("Failed to apply SEB Server connection due to connection mismatch", response); - }); + .ifPresent(w -> { + throw new MoodleResponseException("Failed to apply SEB Server connection due to connection mismatch\n There seems to be another SEB Server already connected to this LMS instance", response); + }); } if (log.isDebugEnabled()) { log.debug("Got warnings from Moodle: {}", response); } + } catch (final MoodleResponseException mre) { + throw mre; } catch (final Exception e) { log.warn("Failed to parse Moodle warnings. Error: {}", e.getMessage()); + throw e; } if (log.isDebugEnabled()) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamUpdateTask.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamUpdateTask.java index 1ce3e244..f926ffdd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamUpdateTask.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamUpdateTask.java @@ -8,6 +8,10 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session; +/** Defines a exam update task. Exam update tasks are called in a fixed time interval on the master + * Webservice instance to update various exam data like state, LMS data and so on. + * A ExamUpdateTask can define a processing order on with the overall scheduler acts. Lower order first processed. + */ public interface ExamUpdateTask { int examUpdateTaskProcessingOrder(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java index b690078b..4ac32f31 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ActivatableEntityController.java @@ -218,7 +218,12 @@ public abstract class ActivatableEntityController { } protected Result notifySaved(final T entity) { + return notifySaved(entity, Activatable.ActivationAction.NONE); + } + + protected Result notifySaved(final T entity, final Activatable.ActivationAction activationAction) { return Result.of(entity); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java index 1b2c0588..96d71a0f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java @@ -10,8 +10,13 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; import javax.validation.Valid; +import ch.ethz.seb.sebserver.gbl.model.Activatable; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsTestService; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; import org.mybatis.dynamic.sql.SqlTable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; @@ -50,8 +55,12 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.LMS_SETUP_ENDPOINT) public class LmsSetupController extends ActivatableEntityController { + private static final Logger log = LoggerFactory.getLogger(LmsSetupController.class); + private final LmsAPIService lmsAPIService; private final LmsTestService lmsTestService; + private final SEBRestrictionService sebRestrictionService; + private final FullLmsIntegrationService fullLmsIntegrationService; final ApplicationEventPublisher applicationEventPublisher; public LmsSetupController( @@ -63,6 +72,8 @@ public class LmsSetupController extends ActivatableEntityController notifySaved(final LmsSetup entity) { - this.applicationEventPublisher.publishEvent(new LmsSetupChangeEvent(entity)); - return super.notifySaved(entity); + protected Result notifySaved(final LmsSetup entity, final Activatable.ActivationAction activation) { + this.applicationEventPublisher.publishEvent(new LmsSetupChangeEvent(entity, activation)); + return super.notifySaved(entity, activation); } + @Override + protected Result validForActivation(final LmsSetup entity, final boolean activation) { + return super.validForActivation(entity, activation) + .map(lmsSetup -> { + if (!activation) { + // on deactivation remove all SEB restrictions and delete full integration if in place + return sebRestrictionService + .applyLMSSetupDeactivation(lmsSetup) + .flatMap(fullLmsIntegrationService::applyLMSSetupDeactivation) + .getOrThrow(); + } + return entity; + }); + } + + @Override + protected Result validForDelete(final LmsSetup entity) { + return Result.tryCatch(() -> { + // if there is a SEB Restriction involved, release all SEB Restriction for exams + if (entity.lmsType.features.contains(LmsSetup.Features.SEB_RESTRICTION)) { + sebRestrictionService + .applyLMSSetupDeactivation(entity) + .getOrThrow(); + } + // if there is a full LMS integration involved, delete it first on LMS + if (entity.lmsType.features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { + fullLmsIntegrationService + .deleteFullLmsIntegration(entity.id) + .getOrThrow(); + } + + return entity; + }); + } } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 2e7d0b5f..6ff660f5 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -347,6 +347,8 @@ sebserver.lmssetup.type.OPEN_EDX=Open edX sebserver.lmssetup.type.ANS_DELFT=Ans Delft sebserver.lmssetup.type.OPEN_OLAT=OLAT + + sebserver.lmssetup.list.actions= sebserver.lmssetup.list.action.no.modify.privilege=No Access: A Assessment Tool Setup from other institution cannot be modified. sebserver.lmssetup.list.empty=No Assessment Tool Setup can be found. Please adapt the filter or create a new Assessment Tool Setup @@ -378,6 +380,7 @@ sebserver.lmssetup.action.test.tokenRequestError=The API access was denied:
sebserver.lmssetup.action.test.quizRequestError=Unable to request courses or exams from the course API of the Assessment Tool. {0} sebserver.lmssetup.action.test.quizRestrictionError=Unable to access course restriction API of the Assessment Tool. {0} sebserver.lmssetup.action.test.features.error=The API access was granted but there is some missing functionality:

- Course Access: {0}
- SEB Restriction: {1} +sebserver.lmssetup.action.test.fullintegration.error=Full Assessment Tool Integration failed due to:

{0} sebserver.lmssetup.action.test.missingParameter=There is one or more missing connection parameter.
Please check the connection parameter for this Assessment Tool Setup sebserver.lmssetup.action.test.unknownError=An unexpected error happened while trying to connect to the Assessment Tool course API. {0} sebserver.lmssetup.action.test.quizRequestError.moodle.missing.plugin=Moodle SEB Server integration plugin cannot be detected on this Moodle server.
Please consider using the origin "Moodle" Assessment Tool Setup type or install the SEB Server integration plugin on this Moodle server.

For further information please refer to the documentation @@ -387,6 +390,7 @@ sebserver.lmssetup.action.save=Save Assessment Tool Setup sebserver.lmssetup.action.activate=Activate Assessment Tool Setup sebserver.lmssetup.action.deactivate=Deactivate Assessment Tool Setup sebserver.lmssetup.action.delete=Delete Assessment Tool Setup +sebserver.lmssetup.action.activation.error=Failed to activate/deactivate Assessment Tool Setup.
Please open the Assessment Tool Setup and test the connection and make sure connection data is correct. sebserver.lmssetup.info.pleaseSelect=At first please select an Assessment Tool Setup from the list diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/JSONParserTest.java b/src/test/java/ch/ethz/seb/sebserver/gbl/JSONParserTest.java new file mode 100644 index 00000000..ef7c2a58 --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/gbl/JSONParserTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl; + +import static org.junit.Assert.assertEquals; + +import java.util.Map; + +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.Test; + +public class JSONParserTest { + + @Test + public void testSpecialChar1() throws JsonProcessingException { + final String json = """ + {"meta_data": { + "param1": "value—dvgswg", + "param2": "value2-dvgswg", + "param3": "value3", + "param4": "value4-%ç/&=&&çETZGFIUZHàPIHBNHK VG$ä$à£à!èéLèPLIOU=(&&(Rç%çE" + }}"""; + + final JSONMapper jsonMapper = new JSONMapper(); + final MetaData metaData = jsonMapper.readValue(json, MetaData.class); + assertEquals("MetaData{meta_data={param1=value—dvgswg, param2=value2-dvgswg, param3=value3, param4=value4-%ç/&=&&çETZGFIUZHàPIHBNHK VG$ä$à£à!èéLèPLIOU=(&&(Rç%çE}}", metaData.toString()); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class MetaData { + @JsonProperty("meta_data") + public final Map meta_data; + + + @JsonCreator + private MetaData( + @JsonProperty("meta_data") final Map metaData) { + + meta_data = metaData; + } + + @Override + public String toString() { + return "MetaData{" + + "meta_data=" + meta_data + + '}'; + } + } +}