From ff89864b19b2d154c814819cb11a8ce0404f9a52 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 2 May 2024 11:39:41 +0200 Subject: [PATCH] SEBSERV-417 create delete exam from Moodle --- .../seb/sebserver/gbl/model/exam/Exam.java | 27 +++ .../model/institution/LmsSetupTestResult.java | 7 +- .../sebserver/gui/content/exam/ExamForm.java | 18 +- .../gui/service/ResourceService.java | 4 +- .../authorization/impl/UserServiceImpl.java | 24 +++ .../bulkaction/impl/DeleteExamAction.java | 8 + .../servicelayer/dao/impl/ExamRecordDAO.java | 6 +- .../servicelayer/exam/ExamAdminService.java | 2 - .../exam/impl/ExamAdminServiceImpl.java | 31 ---- .../servicelayer/lms/LmsAPIService.java | 2 +- .../impl/FullLmsIntegrationServiceImpl.java | 163 +++++++++++++++--- .../lms/impl/LmsAPIServiceImpl.java | 31 +++- .../weblayer/WebServiceSecurityConfig.java | 2 +- .../api/LmsIntegrationController.java | 4 +- 14 files changed, 247 insertions(+), 82 deletions(-) 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 d6bf097f..37d5249e 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 @@ -18,6 +18,8 @@ import java.util.Map; import javax.validation.constraints.NotNull; import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.Entity; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; @@ -415,6 +417,31 @@ public final class Exam implements GrantEntity { return this.additionalAttributes.get(attrName); } + @Override + public Exam printSecureCopy() { + return new Exam( + id, + institutionId, + lmsSetupId, + externalId, + lmsAvailable, + name, + startTime, + endTime, + type, + owner, + supporter, + status, + "--", + sebRestriction, + "--", + active, + lastUpdate, + examTemplateId, + lastModified, + Collections.emptyMap()); + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); 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 11d1980a..0567029b 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 @@ -33,7 +33,8 @@ public final class LmsSetupTestResult { TOKEN_REQUEST, QUIZ_ACCESS_API_REQUEST, QUIZ_RESTRICTION_API_REQUEST, - TEMPLATE_CREATION + TEMPLATE_CREATION, + APPLY_FULL_INTEGRATION, } @JsonProperty(Domain.LMS_SETUP.ATTR_LMS_TYPE) @@ -138,6 +139,10 @@ public final class LmsSetupTestResult { return new LmsSetupTestResult(lmsType, new Error(ErrorType.QUIZ_RESTRICTION_API_REQUEST, message)); } + public static LmsSetupTestResult ofFullIntegrationAPIError(final LmsSetup.LmsType lmsType, final String message) { + return new LmsSetupTestResult(lmsType, new Error(ErrorType.APPLY_FULL_INTEGRATION, message)); + } + public final static class Error { @JsonProperty(ATTR_ERROR_TYPE) 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 05127134..87205940 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 @@ -18,6 +18,7 @@ import java.util.stream.Stream; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.*; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.swt.layout.GridData; @@ -204,7 +205,7 @@ public class ExamForm implements TemplateComposer { 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); + final boolean sebRestrictionAvailable = readonly && hasSEBRestrictionAPI(exam); final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService .getBuilder(CheckSEBRestriction.class) .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()) @@ -742,25 +743,22 @@ public class ExamForm implements TemplateComposer { throw new RuntimeException("Error while handle exam import setup failure:", e); } - private boolean testSEBRestrictionAPI(final Exam exam) { + private boolean hasSEBRestrictionAPI(final Exam exam) { if (exam.lmsSetupId == null || !exam.isLmsAvailable() || exam.status == ExamStatus.ARCHIVED) { return false; } - // Call the testing endpoint with the specified data to test - final Result result = this.restService.getBuilder(TestLmsSetup.class) + // get LMSSetup + final Result call = this.restService.getBuilder(GetLmsSetup.class) .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId)) .call(); - if (result.hasError()) { + if (call.hasError()) { return false; } - final LmsSetupTestResult lmsSetupTestResult = result.get(); - if (!lmsSetupTestResult.lmsType.features.contains(LmsSetup.Features.SEB_RESTRICTION)) { - return false; - } - return !lmsSetupTestResult.hasError(ErrorType.QUIZ_RESTRICTION_API_REQUEST); + final LmsSetup lmsSetup = call.get(); + return (lmsSetup.getLmsType().features.contains(LmsSetup.Features.SEB_RESTRICTION)); } private void showConsistencyChecks( diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java index c12c7892..02bc8f1e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java @@ -90,7 +90,7 @@ import ch.ethz.seb.sebserver.gui.service.session.MonitoringEntry; @GuiProfile /** Defines functionality to get resources or functions of resources to feed e.g. selection or * combo-box content. - * */ + */ public class ResourceService { private static final Logger log = LoggerFactory.getLogger(ResourceService.class); @@ -653,7 +653,7 @@ public class ResourceService { } public String localizedClientConnectionStatusName(final ConnectionStatus status) { - String name; + final String name; if (status != null) { name = status.name(); } else { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java index 01090043..29f95ca9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java @@ -116,6 +116,10 @@ public class UserServiceImpl implements UserService { public SEBServerUser extract(final Principal principal) { if (principal instanceof OAuth2Authentication) { final Authentication userAuthentication = ((OAuth2Authentication) principal).getUserAuthentication(); + if (userAuthentication == null) { + // check if lms integration client + return isLMSIntegrationClient(principal); + } if (userAuthentication instanceof UsernamePasswordAuthenticationToken) { final Object userPrincipal = userAuthentication.getPrincipal(); if (userPrincipal instanceof SEBServerUser) { @@ -128,6 +132,26 @@ public class UserServiceImpl implements UserService { } } + private static SEBServerUser isLMSIntegrationClient(final Principal principal) { + final String name = principal.getName(); + if ("lmsClient".equals(name)) { + return new SEBServerUser( + -1L, + new UserInfo("LMS_INTEGRATION_CLIENT", -1L, null, "lmsIntegrationClient", "lmsIntegrationClient", "lmsIntegrationClient", null, + false, + false, + true, + null, null, + Arrays.stream(UserRole.values()) + .map(Enum::name) + .collect(Collectors.toSet()), + Collections.emptyList(), + Collections.emptyList()), + null); + } + return null; + } + // 2. Separated thread strategy @Lazy @Component diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java index a73aef0f..8fc840b4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java @@ -99,6 +99,14 @@ public class DeleteExamAction implements BatchActionExec { .onError(TransactionHandler::rollback); } + @Transactional + public Result deleteExamFromLMSIntegration(final Exam exam) { + return deleteExamDependencies(exam) + .flatMap(this::deleteExamWithRefs) + .map(Exam::getEntityKey) + .onError(TransactionHandler::rollback); + } + private Result deleteExamDependencies(final Exam entity) { return this.clientConnectionDAO.deleteAllForExam(entity.id) .map(this::logDelete) 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 3cba1249..ba763705 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 @@ -126,10 +126,14 @@ public class ExamRecordDAO { @Transactional(readOnly = true) public Result> allInstitutionIdsByQuizId(final String quizId) { return Result.tryCatch(() -> { + if (StringUtils.isBlank(quizId)) { + return Collections.emptyList(); + } + return this.examRecordMapper.selectByExample() .where( ExamRecordDynamicSqlSupport.externalId, - isEqualToWhenPresent(quizId)) + isEqualTo(quizId)) .and( ExamRecordDynamicSqlSupport.active, isEqualToWhenPresent(BooleanUtils.toIntegerObject(true))) 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 44baf3a3..d2408ed8 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 @@ -150,6 +150,4 @@ public interface ExamAdminService { Result applyQuitPassword(Exam exam); - Result findExamByLmsIdentity(String courseId, String quizId, String identity); - } 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 c67d9474..3a79c76f 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 @@ -385,37 +385,6 @@ public class ExamAdminServiceImpl implements ExamAdminService { .onError(t -> log.error("Failed to update SEB Client restriction for Exam: {}", exam, t)); } - - - @Override - public Result findExamByLmsIdentity( - final String courseId, - final String quizId, - final String identity) { - - for (final LmsType lmsType : LmsType.values()) { - switch (lmsType) { - case MOODLE_PLUGIN -> { - if (StringUtils.isBlank(quizId) || StringUtils.isBlank(courseId)) { - return Result.ofError(new APIMessageException( - APIMessage.ErrorMessage.FIELD_VALIDATION.of("Missing courseId or quizId"))); - } - - return examDAO.byExternalIdLike(MoodleUtils.getInternalQuizId( - quizId, - courseId, - Constants.PERCENTAGE_STRING, - Constants.PERCENTAGE_STRING)); - } - // TODO add other LMS types if they support full integration - } - } - - return Result.ofError( - new ResourceNotFoundException(EntityType.EXAM, - "Not found by LMS identity [" + courseId + "|"+ quizId+ "|"+ identity + "]")); - } - private Result initAdditionalAttributesForMoodleExams(final Exam exam) { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java index 4372c49e..abeb31f5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java @@ -72,7 +72,7 @@ public interface LmsAPIService { * @return LmsAPITemplate for specified LmsSetup configuration */ Result getLmsAPITemplate(String lmsSetupId); - /** use this to the the specified LmsAPITemplate. + /** use this to the specified LmsAPITemplate. * * @param template the LmsAPITemplate * @return LmsSetupTestResult containing list of errors if happened */ 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 c7287881..8fcc0ec1 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 @@ -10,15 +10,14 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.function.Function; import java.util.stream.Collectors; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; -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.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; @@ -26,12 +25,14 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate; 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.sebconfig.SEBClientConfig; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamTemplateDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.DeleteExamAction; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateChangeEvent; @@ -39,7 +40,10 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationServi import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -57,33 +61,52 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService private static final Logger log = LoggerFactory.getLogger(FullLmsIntegrationServiceImpl.class); private final LmsSetupDAO lmsSetupDAO; + private final UserActivityLogDAO userActivityLogDAO; + private final SEBClientConfigDAO sebClientConfigDAO; + private final ClientConfigService clientConfigService; + private final DeleteExamAction deleteExamAction; private final LmsAPIService lmsAPIService; private final ExamAdminService examAdminService; + private final ExamSessionService examSessionService; private final ExamDAO examDAO; private final ExamTemplateDAO examTemplateDAO; private final WebserviceInfo webserviceInfo; - + private final String lmsAPIEndpoint; + private final UserService userService; private final ClientCredentialsResourceDetails resource; - private final OAuth2RestTemplate restTemplate; public FullLmsIntegrationServiceImpl( final LmsSetupDAO lmsSetupDAO, + final UserActivityLogDAO userActivityLogDAO, + final SEBClientConfigDAO sebClientConfigDAO, + final ClientConfigService clientConfigService, + final DeleteExamAction deleteExamAction, final LmsAPIService lmsAPIService, final ExamAdminService examAdminService, + final ExamSessionService examSessionService, final ExamDAO examDAO, final ExamTemplateDAO examTemplateDAO, final WebserviceInfo webserviceInfo, final ClientHttpRequestFactoryService clientHttpRequestFactoryService, + final UserService userService, + @Value("${sebserver.webservice.lms.api.endpoint}") final String lmsAPIEndpoint, @Value("${sebserver.webservice.lms.api.clientId}") final String clientId, @Value("${sebserver.webservice.api.admin.clientSecret}") final String clientSecret) { this.lmsSetupDAO = lmsSetupDAO; + this.userActivityLogDAO = userActivityLogDAO; + this.sebClientConfigDAO = sebClientConfigDAO; + this.clientConfigService = clientConfigService; + this.deleteExamAction = deleteExamAction; this.lmsAPIService = lmsAPIService; this.examAdminService = examAdminService; + this.examSessionService = examSessionService; this.examDAO = examDAO; this.examTemplateDAO = examTemplateDAO; this.webserviceInfo = webserviceInfo; + this.lmsAPIEndpoint = lmsAPIEndpoint; + this.userService = userService; resource = new ClientCredentialsResourceDetails(); resource.setAccessTokenUri(webserviceInfo.getOAuthTokenURI()); @@ -163,7 +186,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService final IntegrationData data = new IntegrationData( connectionId, lmsSetup.name, - webserviceInfo.getExternalServerURL(), + getAPIRootURL(), accessToken, this.getIntegrationTemplates(lmsSetup.institutionId) ); @@ -181,6 +204,8 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService }); } + + @Override public Result deleteFullLmsIntegration(final Long lmsSetupId) { return lmsSetupDAO @@ -219,8 +244,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService .getLmsSetupIdByConnectionId(lmsUUID) .flatMap(lmsAPIService::getLmsAPITemplate) .map(findQuizData(courseId, quizId)) - .map(createExam(examTemplateId, quitPassword)) - .map(this::createAdHocSupporterAccount); + .map(createAccountAndExam(examTemplateId, quitPassword)); } @Override @@ -229,8 +253,27 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService final String courseId, final String quizId) { - return findExam(courseId, quizId) - .flatMap(exam -> examDAO.deleteOne(exam.id)); + return lmsSetupDAO + .getLmsSetupIdByConnectionId(lmsUUID) + .flatMap(lmsAPIService::getLmsAPITemplate) + .map(findQuizData(courseId, quizId)) + .flatMap(this::findExam) + .map(this::checkDeletion) + .map(this::logExamDeleted) + .flatMap(deleteExamAction::deleteExamFromLMSIntegration); + } + + private Exam checkDeletion(final Exam exam) { + // TODO check if Exam can be deleted according to the Spec + + // check if there are no active SEB client connections + if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) { + throw new APIMessage.APIMessageException( + APIMessage.ErrorMessage.INTEGRITY_VALIDATION + .of("Exam currently has active SEB Client connections.")); + } + + return exam; } @Override @@ -244,7 +287,39 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService final String courseId, final String quizId, final OutputStream out) { - return Result.ofRuntimeError("TODO"); + + try { + + 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(); + + String connectionConfigId = exam.getAdditionalAttribute(Exam.ADDITIONAL_ATTR_DEFAULT_CONNECTION_CONFIGURATION); + if (StringUtils.isBlank(connectionConfigId)) { + connectionConfigId = this.sebClientConfigDAO + .all(exam.institutionId, true) + .map(all -> all.iterator().next()) + .map(SEBClientConfig::getModelId) + .getOr(null); + } + if (StringUtils.isBlank(connectionConfigId)) { + return Result.ofRuntimeError("No active Connection Configuration found"); + } + + this.clientConfigService.exportSEBClientConfiguration(out, connectionConfigId, exam.id); + return Result.EMPTY; + + } catch (final Exception e) { + return Result.ofError(e); + } } private Function findQuizData( @@ -260,42 +335,64 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService return lmsAPITemplate .getQuiz(internalQuizId) - .getOrThrow(); + .onError(error -> log.error("Failed to find quiz-data for id: {}", quizId)) + // this is only for debugging until Moodle Plugin is ready + .getOr(new QuizData( + MoodleUtils.getInternalQuizId(quizId, courseId, "MoodlePluginMockQuiz", null), + lmsAPITemplate.lmsSetup().institutionId, + lmsAPITemplate.lmsSetup().id, + lmsAPITemplate.lmsSetup().lmsType, + "MoodlePluginMockQuiz", + "", + DateTime.now(), + DateTime.now().plusDays(1), + "https://mockmoodle/swvgfrwef.sdvw", + null + )); }; } - private Result findExam( - final String courseId, - final String quizId) { - - final String externalIdLike = quizId + Constants.COLON + courseId + Constants.PERCENTAGE; - return examDAO.byExternalIdLike(externalIdLike); + private Result findExam(final QuizData quizData) { + return examDAO.byExternalIdLike(quizData.id); } - private Function createExam( + private Function createAccountAndExam( final String examTemplateId, final String quitPassword) { return quizData -> { + final SEBServerUser currentUser = userService.getCurrentUser(); + + // check if the exam has already been imported, If so return the existing exam + final Result existingExam = findExam(quizData); + if (!existingExam.hasError()) { + // TODO do we need to check if ad-hoc account exists and if not, create one? + return existingExam.get(); + } + + // import exam final POSTMapper post = new POSTMapper(null, null); post.putIfAbsent(Domain.EXAM.ATTR_EXAM_TEMPLATE_ID, examTemplateId); if (StringUtils.isNotBlank(quitPassword)) { post.putIfAbsent(Domain.EXAM.ATTR_QUIT_PASSWORD, quitPassword); } - final Exam exam = new Exam(null, quizData, post); + final String accountUUID = createAdHocSupporterAccount(quizData); + post.putIfAbsent(Domain.EXAM.ATTR_OWNER, accountUUID); + final Exam exam = new Exam(null, quizData, post); return examDAO .createNew(exam) .flatMap(examAdminService::applyExamImportInitialization) + .map(this::logExamCreated) .getOrThrow(); }; } - private Exam createAdHocSupporterAccount(final Exam exam) { + private String createAdHocSupporterAccount(final QuizData data) { // TODO create an ad hoc supporter account for this exam and apply it to the exam - return exam; + return "mockAccountUUID"; } private void deleteAdHocAccount(final Long examId) { @@ -319,4 +416,22 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService .getOrThrow(); } + private String getAPIRootURL() { + return webserviceInfo.getExternalServerURL() + lmsAPIEndpoint; + } + + private Exam logExamCreated(final Exam exam) { + this.userActivityLogDAO + .logCreate(exam) + .onError(error -> log.warn("Failed to log exam creation from LMS: {}", error.getMessage())); + return exam; + } + + private Exam logExamDeleted(final Exam exam) { + this.userActivityLogDAO + .logDelete(exam) + .onError(error -> log.warn("Failed to log exam deletion from LMS: {}", error.getMessage())); + return exam; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java index bb1e69a7..f948d504 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java @@ -16,9 +16,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Collectors; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.*; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; @@ -38,11 +40,6 @@ import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateFactory; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.QuizLookupService; @Lazy @Service @@ -56,6 +53,7 @@ public class LmsAPIServiceImpl implements LmsAPIService { private final ClientCredentialService clientCredentialService; private final QuizLookupService quizLookupService; private final EnumMap templateFactories; + private final ApplicationEventPublisher applicationEventPublisher; private final Map cache = new ConcurrentHashMap<>(); @@ -64,17 +62,19 @@ public class LmsAPIServiceImpl implements LmsAPIService { final LmsSetupDAO lmsSetupDAO, final ClientCredentialService clientCredentialService, final QuizLookupService quizLookupService, + final ApplicationEventPublisher applicationEventPublisher, final Collection lmsAPITemplateFactories) { this.webserviceInfo = webserviceInfo; this.lmsSetupDAO = lmsSetupDAO; this.clientCredentialService = clientCredentialService; this.quizLookupService = quizLookupService; + this.applicationEventPublisher = applicationEventPublisher; final Map factories = lmsAPITemplateFactories .stream() .collect(Collectors.toMap( - t -> t.lmsType(), + LmsAPITemplateFactory::lmsType, Function.identity())); this.templateFactories = new EnumMap<>(factories); } @@ -158,14 +158,31 @@ public class LmsAPIServiceImpl implements LmsAPIService { this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0)); return lmsSetupTestResult; } - } 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()) { this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0)); + this.lmsSetupDAO + .setIntegrationActive(lmsSetupId, false) + .onError(er -> log.error("Failed to mark LMS integration inactive", er)); return lmsSetupTestResult; + } else { + // try to apply full integration with a change LMSSetup notification + try { + applicationEventPublisher.publishEvent(new LmsSetupChangeEvent(template.lmsSetup())); + return lmsSetupTestResult; + } catch (final Exception e) { + log.warn( + "Failed to apply full LMS integration on test attempt: lms: {} error: {}", + template.lmsSetup(), + e.getMessage()); + return LmsSetupTestResult.ofFullIntegrationAPIError( + template.lmsSetup().lmsType, + "Failed to apply full LMS integration"); + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java index 98253975..cb406d2a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java @@ -270,7 +270,7 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { log.info("Redirect to login after unauthorized request"); response.getOutputStream().println("{ \"error\": \"" + exception.getMessage() + "\" }"); }, - EXAM_API_RESOURCE_ID, + LMS_API_RESOURCE_ID, apiEndpoint, true, 4, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java index 17f5e4e8..4c4a2d6d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java @@ -84,8 +84,8 @@ public class LmsIntegrationController { } @RequestMapping( - path = API.LMS_FULL_INTEGRATION_EXAM_ENDPOINT, - method = RequestMethod.DELETE, + path = API.LMS_FULL_INTEGRATION_CONNECTION_CONFIG_ENDPOINT, + method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public void getConnectionConfiguration(