SEBSERV-417 create delete exam from Moodle

This commit is contained in:
anhefti 2024-05-02 11:39:41 +02:00
parent c0919ce0cf
commit ff89864b19
14 changed files with 247 additions and 82 deletions

View file

@ -18,6 +18,8 @@ import java.util.Map;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import ch.ethz.seb.sebserver.gbl.api.API; 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.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -415,6 +417,31 @@ public final class Exam implements GrantEntity {
return this.additionalAttributes.get(attrName); 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 @Override
public String toString() { public String toString() {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();

View file

@ -33,7 +33,8 @@ public final class LmsSetupTestResult {
TOKEN_REQUEST, TOKEN_REQUEST,
QUIZ_ACCESS_API_REQUEST, QUIZ_ACCESS_API_REQUEST,
QUIZ_RESTRICTION_API_REQUEST, QUIZ_RESTRICTION_API_REQUEST,
TEMPLATE_CREATION TEMPLATE_CREATION,
APPLY_FULL_INTEGRATION,
} }
@JsonProperty(Domain.LMS_SETUP.ATTR_LMS_TYPE) @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)); 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 { public final static class Error {
@JsonProperty(ATTR_ERROR_TYPE) @JsonProperty(ATTR_ERROR_TYPE)

View file

@ -18,6 +18,7 @@ import java.util.stream.Stream;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures; 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.exam.*;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.layout.GridData; 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 editable = modifyGrant && (exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.RUNNING);
final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean( final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); 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 final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
.getBuilder(CheckSEBRestriction.class) .getBuilder(CheckSEBRestriction.class)
.withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()) .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); 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) { if (exam.lmsSetupId == null || !exam.isLmsAvailable() || exam.status == ExamStatus.ARCHIVED) {
return false; return false;
} }
// Call the testing endpoint with the specified data to test // get LMSSetup
final Result<LmsSetupTestResult> result = this.restService.getBuilder(TestLmsSetup.class) final Result<LmsSetup> call = this.restService.getBuilder(GetLmsSetup.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId)) .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId))
.call(); .call();
if (result.hasError()) { if (call.hasError()) {
return false; return false;
} }
final LmsSetupTestResult lmsSetupTestResult = result.get(); final LmsSetup lmsSetup = call.get();
if (!lmsSetupTestResult.lmsType.features.contains(LmsSetup.Features.SEB_RESTRICTION)) { return (lmsSetup.getLmsType().features.contains(LmsSetup.Features.SEB_RESTRICTION));
return false;
}
return !lmsSetupTestResult.hasError(ErrorType.QUIZ_RESTRICTION_API_REQUEST);
} }
private void showConsistencyChecks( private void showConsistencyChecks(

View file

@ -90,7 +90,7 @@ import ch.ethz.seb.sebserver.gui.service.session.MonitoringEntry;
@GuiProfile @GuiProfile
/** Defines functionality to get resources or functions of resources to feed e.g. selection or /** Defines functionality to get resources or functions of resources to feed e.g. selection or
* combo-box content. * combo-box content.
* */ */
public class ResourceService { public class ResourceService {
private static final Logger log = LoggerFactory.getLogger(ResourceService.class); private static final Logger log = LoggerFactory.getLogger(ResourceService.class);
@ -653,7 +653,7 @@ public class ResourceService {
} }
public String localizedClientConnectionStatusName(final ConnectionStatus status) { public String localizedClientConnectionStatusName(final ConnectionStatus status) {
String name; final String name;
if (status != null) { if (status != null) {
name = status.name(); name = status.name();
} else { } else {

View file

@ -116,6 +116,10 @@ public class UserServiceImpl implements UserService {
public SEBServerUser extract(final Principal principal) { public SEBServerUser extract(final Principal principal) {
if (principal instanceof OAuth2Authentication) { if (principal instanceof OAuth2Authentication) {
final Authentication userAuthentication = ((OAuth2Authentication) principal).getUserAuthentication(); final Authentication userAuthentication = ((OAuth2Authentication) principal).getUserAuthentication();
if (userAuthentication == null) {
// check if lms integration client
return isLMSIntegrationClient(principal);
}
if (userAuthentication instanceof UsernamePasswordAuthenticationToken) { if (userAuthentication instanceof UsernamePasswordAuthenticationToken) {
final Object userPrincipal = userAuthentication.getPrincipal(); final Object userPrincipal = userAuthentication.getPrincipal();
if (userPrincipal instanceof SEBServerUser) { 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 // 2. Separated thread strategy
@Lazy @Lazy
@Component @Component

View file

@ -99,6 +99,14 @@ public class DeleteExamAction implements BatchActionExec {
.onError(TransactionHandler::rollback); .onError(TransactionHandler::rollback);
} }
@Transactional
public Result<EntityKey> deleteExamFromLMSIntegration(final Exam exam) {
return deleteExamDependencies(exam)
.flatMap(this::deleteExamWithRefs)
.map(Exam::getEntityKey)
.onError(TransactionHandler::rollback);
}
private Result<Exam> deleteExamDependencies(final Exam entity) { private Result<Exam> deleteExamDependencies(final Exam entity) {
return this.clientConnectionDAO.deleteAllForExam(entity.id) return this.clientConnectionDAO.deleteAllForExam(entity.id)
.map(this::logDelete) .map(this::logDelete)

View file

@ -126,10 +126,14 @@ public class ExamRecordDAO {
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<Long>> allInstitutionIdsByQuizId(final String quizId) { public Result<Collection<Long>> allInstitutionIdsByQuizId(final String quizId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
if (StringUtils.isBlank(quizId)) {
return Collections.emptyList();
}
return this.examRecordMapper.selectByExample() return this.examRecordMapper.selectByExample()
.where( .where(
ExamRecordDynamicSqlSupport.externalId, ExamRecordDynamicSqlSupport.externalId,
isEqualToWhenPresent(quizId)) isEqualTo(quizId))
.and( .and(
ExamRecordDynamicSqlSupport.active, ExamRecordDynamicSqlSupport.active,
isEqualToWhenPresent(BooleanUtils.toIntegerObject(true))) isEqualToWhenPresent(BooleanUtils.toIntegerObject(true)))

View file

@ -150,6 +150,4 @@ public interface ExamAdminService {
Result<Exam> applyQuitPassword(Exam exam); Result<Exam> applyQuitPassword(Exam exam);
Result<Exam> findExamByLmsIdentity(String courseId, String quizId, String identity);
} }

View file

@ -385,37 +385,6 @@ public class ExamAdminServiceImpl implements ExamAdminService {
.onError(t -> log.error("Failed to update SEB Client restriction for Exam: {}", exam, t)); .onError(t -> log.error("Failed to update SEB Client restriction for Exam: {}", exam, t));
} }
@Override
public Result<Exam> 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<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) { private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {

View file

@ -72,7 +72,7 @@ public interface LmsAPIService {
* @return LmsAPITemplate for specified LmsSetup configuration */ * @return LmsAPITemplate for specified LmsSetup configuration */
Result<LmsAPITemplate> getLmsAPITemplate(String lmsSetupId); Result<LmsAPITemplate> getLmsAPITemplate(String lmsSetupId);
/** use this to the the specified LmsAPITemplate. /** use this to the specified LmsAPITemplate.
* *
* @param template the LmsAPITemplate * @param template the LmsAPITemplate
* @return LmsSetupTestResult containing list of errors if happened */ * @return LmsSetupTestResult containing list of errors if happened */

View file

@ -10,15 +10,14 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; 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.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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.ExamTemplate;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamTemplateDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; 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.dao.impl.ExamDeletionEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateChangeEvent; 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.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; 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.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.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; 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 static final Logger log = LoggerFactory.getLogger(FullLmsIntegrationServiceImpl.class);
private final LmsSetupDAO lmsSetupDAO; 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 LmsAPIService lmsAPIService;
private final ExamAdminService examAdminService; private final ExamAdminService examAdminService;
private final ExamSessionService examSessionService;
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final ExamTemplateDAO examTemplateDAO; private final ExamTemplateDAO examTemplateDAO;
private final WebserviceInfo webserviceInfo; private final WebserviceInfo webserviceInfo;
private final String lmsAPIEndpoint;
private final UserService userService;
private final ClientCredentialsResourceDetails resource; private final ClientCredentialsResourceDetails resource;
private final OAuth2RestTemplate restTemplate; private final OAuth2RestTemplate restTemplate;
public FullLmsIntegrationServiceImpl( public FullLmsIntegrationServiceImpl(
final LmsSetupDAO lmsSetupDAO, final LmsSetupDAO lmsSetupDAO,
final UserActivityLogDAO userActivityLogDAO,
final SEBClientConfigDAO sebClientConfigDAO,
final ClientConfigService clientConfigService,
final DeleteExamAction deleteExamAction,
final LmsAPIService lmsAPIService, final LmsAPIService lmsAPIService,
final ExamAdminService examAdminService, final ExamAdminService examAdminService,
final ExamSessionService examSessionService,
final ExamDAO examDAO, final ExamDAO examDAO,
final ExamTemplateDAO examTemplateDAO, final ExamTemplateDAO examTemplateDAO,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, 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.lms.api.clientId}") final String clientId,
@Value("${sebserver.webservice.api.admin.clientSecret}") final String clientSecret) { @Value("${sebserver.webservice.api.admin.clientSecret}") final String clientSecret) {
this.lmsSetupDAO = lmsSetupDAO; this.lmsSetupDAO = lmsSetupDAO;
this.userActivityLogDAO = userActivityLogDAO;
this.sebClientConfigDAO = sebClientConfigDAO;
this.clientConfigService = clientConfigService;
this.deleteExamAction = deleteExamAction;
this.lmsAPIService = lmsAPIService; this.lmsAPIService = lmsAPIService;
this.examAdminService = examAdminService; this.examAdminService = examAdminService;
this.examSessionService = examSessionService;
this.examDAO = examDAO; this.examDAO = examDAO;
this.examTemplateDAO = examTemplateDAO; this.examTemplateDAO = examTemplateDAO;
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.lmsAPIEndpoint = lmsAPIEndpoint;
this.userService = userService;
resource = new ClientCredentialsResourceDetails(); resource = new ClientCredentialsResourceDetails();
resource.setAccessTokenUri(webserviceInfo.getOAuthTokenURI()); resource.setAccessTokenUri(webserviceInfo.getOAuthTokenURI());
@ -163,7 +186,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final IntegrationData data = new IntegrationData( final IntegrationData data = new IntegrationData(
connectionId, connectionId,
lmsSetup.name, lmsSetup.name,
webserviceInfo.getExternalServerURL(), getAPIRootURL(),
accessToken, accessToken,
this.getIntegrationTemplates(lmsSetup.institutionId) this.getIntegrationTemplates(lmsSetup.institutionId)
); );
@ -181,6 +204,8 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
}); });
} }
@Override @Override
public Result<Boolean> deleteFullLmsIntegration(final Long lmsSetupId) { public Result<Boolean> deleteFullLmsIntegration(final Long lmsSetupId) {
return lmsSetupDAO return lmsSetupDAO
@ -219,8 +244,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.getLmsSetupIdByConnectionId(lmsUUID) .getLmsSetupIdByConnectionId(lmsUUID)
.flatMap(lmsAPIService::getLmsAPITemplate) .flatMap(lmsAPIService::getLmsAPITemplate)
.map(findQuizData(courseId, quizId)) .map(findQuizData(courseId, quizId))
.map(createExam(examTemplateId, quitPassword)) .map(createAccountAndExam(examTemplateId, quitPassword));
.map(this::createAdHocSupporterAccount);
} }
@Override @Override
@ -229,8 +253,27 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final String courseId, final String courseId,
final String quizId) { final String quizId) {
return findExam(courseId, quizId) return lmsSetupDAO
.flatMap(exam -> examDAO.deleteOne(exam.id)); .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 @Override
@ -244,7 +287,39 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final String courseId, final String courseId,
final String quizId, final String quizId,
final OutputStream out) { final OutputStream out) {
return Result.ofRuntimeError("TODO");
try {
final Result<Exam> 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<LmsAPITemplate, QuizData> findQuizData( private Function<LmsAPITemplate, QuizData> findQuizData(
@ -260,42 +335,64 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return lmsAPITemplate return lmsAPITemplate
.getQuiz(internalQuizId) .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<Exam> findExam( private Result<Exam> findExam(final QuizData quizData) {
final String courseId, return examDAO.byExternalIdLike(quizData.id);
final String quizId) {
final String externalIdLike = quizId + Constants.COLON + courseId + Constants.PERCENTAGE;
return examDAO.byExternalIdLike(externalIdLike);
} }
private Function<QuizData, Exam> createExam( private Function<QuizData, Exam> createAccountAndExam(
final String examTemplateId, final String examTemplateId,
final String quitPassword) { final String quitPassword) {
return quizData -> { return quizData -> {
final SEBServerUser currentUser = userService.getCurrentUser();
// check if the exam has already been imported, If so return the existing exam
final Result<Exam> 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); final POSTMapper post = new POSTMapper(null, null);
post.putIfAbsent(Domain.EXAM.ATTR_EXAM_TEMPLATE_ID, examTemplateId); post.putIfAbsent(Domain.EXAM.ATTR_EXAM_TEMPLATE_ID, examTemplateId);
if (StringUtils.isNotBlank(quitPassword)) { if (StringUtils.isNotBlank(quitPassword)) {
post.putIfAbsent(Domain.EXAM.ATTR_QUIT_PASSWORD, 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 return examDAO
.createNew(exam) .createNew(exam)
.flatMap(examAdminService::applyExamImportInitialization) .flatMap(examAdminService::applyExamImportInitialization)
.map(this::logExamCreated)
.getOrThrow(); .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 // 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) { private void deleteAdHocAccount(final Long examId) {
@ -319,4 +416,22 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.getOrThrow(); .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;
}
} }

View file

@ -16,9 +16,11 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.*;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; 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.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; 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.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 @Lazy
@Service @Service
@ -56,6 +53,7 @@ public class LmsAPIServiceImpl implements LmsAPIService {
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final QuizLookupService quizLookupService; private final QuizLookupService quizLookupService;
private final EnumMap<LmsType, LmsAPITemplateFactory> templateFactories; private final EnumMap<LmsType, LmsAPITemplateFactory> templateFactories;
private final ApplicationEventPublisher applicationEventPublisher;
private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>(); private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>();
@ -64,17 +62,19 @@ public class LmsAPIServiceImpl implements LmsAPIService {
final LmsSetupDAO lmsSetupDAO, final LmsSetupDAO lmsSetupDAO,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final QuizLookupService quizLookupService, final QuizLookupService quizLookupService,
final ApplicationEventPublisher applicationEventPublisher,
final Collection<LmsAPITemplateFactory> lmsAPITemplateFactories) { final Collection<LmsAPITemplateFactory> lmsAPITemplateFactories) {
this.webserviceInfo = webserviceInfo; this.webserviceInfo = webserviceInfo;
this.lmsSetupDAO = lmsSetupDAO; this.lmsSetupDAO = lmsSetupDAO;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.quizLookupService = quizLookupService; this.quizLookupService = quizLookupService;
this.applicationEventPublisher = applicationEventPublisher;
final Map<LmsType, LmsAPITemplateFactory> factories = lmsAPITemplateFactories final Map<LmsType, LmsAPITemplateFactory> factories = lmsAPITemplateFactories
.stream() .stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
t -> t.lmsType(), LmsAPITemplateFactory::lmsType,
Function.identity())); Function.identity()));
this.templateFactories = new EnumMap<>(factories); this.templateFactories = new EnumMap<>(factories);
} }
@ -158,14 +158,31 @@ public class LmsAPIServiceImpl implements LmsAPIService {
this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0)); this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0));
return lmsSetupTestResult; return lmsSetupTestResult;
} }
} }
if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) {
final Long lmsSetupId = template.lmsSetup().id;
final LmsSetupTestResult lmsSetupTestResult = template.testFullIntegrationAPI(); final LmsSetupTestResult lmsSetupTestResult = template.testFullIntegrationAPI();
if (!lmsSetupTestResult.isOk()) { if (!lmsSetupTestResult.isOk()) {
this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0)); 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; 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");
}
} }
} }

View file

@ -270,7 +270,7 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter {
log.info("Redirect to login after unauthorized request"); log.info("Redirect to login after unauthorized request");
response.getOutputStream().println("{ \"error\": \"" + exception.getMessage() + "\" }"); response.getOutputStream().println("{ \"error\": \"" + exception.getMessage() + "\" }");
}, },
EXAM_API_RESOURCE_ID, LMS_API_RESOURCE_ID,
apiEndpoint, apiEndpoint,
true, true,
4, 4,

View file

@ -84,8 +84,8 @@ public class LmsIntegrationController {
} }
@RequestMapping( @RequestMapping(
path = API.LMS_FULL_INTEGRATION_EXAM_ENDPOINT, path = API.LMS_FULL_INTEGRATION_CONNECTION_CONFIG_ENDPOINT,
method = RequestMethod.DELETE, method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void getConnectionConfiguration( public void getConnectionConfiguration(