SEBSERV-417 impl still in progress

This commit is contained in:
anhefti 2024-04-15 08:46:55 +02:00
parent a81447601b
commit 5cc7624f73
21 changed files with 458 additions and 313 deletions

View file

@ -25,6 +25,12 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCac
/** Concrete EntityDAO interface of Exam entities */
public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSupportDAO<Exam> {
/** Use this to find an imported Exam with external id with like match
*
* @param internalQuizIdLike like match string that will be applied to like SQL match
* @return Result refer to Exam that matches the query. If more then one matches, error is reported.*/
Result<Exam> byExternalIdLike(String internalQuizIdLike);
/** Get a GrantEntity for the exam of specified id (PK)
* This is actually a Exam instance but with no course data loaded.
*
@ -230,4 +236,6 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
void markLMSAvailability(final String externalQuizId, final boolean available, final String updateId);
void updateQuitPassword(Exam exam, String quitPassword);
}

View file

@ -31,10 +31,16 @@ public interface LmsSetupDAO extends ActivatableEntityDAO<LmsSetup, LmsSetup>, B
Result<ProxyData> getLmsAPIAccessProxyData(String lmsSetupId);
/** Checks if the given LmsSetup instance is in sync with the version on
* data base by matching the update_time field
* database by matching the update_time field
*
* @param lmsSetup LmsSetup instance to check if it is up to date
* @param lmsSetup LmsSetup instance to check if it is up-to-date
* @return true if the update_time has the same value on persistent */
boolean isUpToDate(LmsSetup lmsSetup);
/** This wil find the internal LMSSetup with the universal connectionId if it exists on this server.
*
* @param connectionId The connectionId UUID (unique ID)
* @return the local LMSSetup DB PK ID */
Result<Long> getLmsSetupIdByConnectionId(String connectionId);
}

View file

@ -23,6 +23,7 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.dynamic.sql.update.UpdateDSL;
@ -54,10 +55,6 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
@Lazy
@Component
@ -93,6 +90,27 @@ public class ExamDAOImpl implements ExamDAO {
.flatMap(this::toDomainModel);
}
@Override
@Transactional(readOnly = true)
public Result<Exam> byExternalIdLike(final String internalQuizIdLike) {
return Result.tryCatch(() -> {
final List<ExamRecord> execute = examRecordMapper.selectByExample()
.where(externalId, isLike(internalQuizIdLike))
.build()
.execute();
if (execute == null || execute.isEmpty()) {
throw new NoResourceFoundException(EntityType.EXAM, "No exam found for external_id like" + internalQuizIdLike);
}
if (execute.size() > 1) {
throw new IllegalStateException("To many exams found for external_id like" + internalQuizIdLike);
}
return execute.get(0);
}).flatMap(this::toDomainModel);
}
@Override
public Result<GrantEntity> examGrantEntityByPK(final Long id) {
return this.examRecordDAO.recordById(id)

View file

@ -14,6 +14,7 @@ import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.dynamic.sql.SqlBuilder;
@ -41,11 +42,6 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDyn
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.LmsSetupRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.BulkAction;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport;
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.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
@Lazy
@Component
@ -75,6 +71,23 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
.flatMap(this::toDomainModel);
}
@Override
@Transactional(readOnly = true)
public Result<Long> getLmsSetupIdByConnectionId(final String connectionId) {
return Result.tryCatch(() -> {
final List<Long> find = lmsSetupRecordMapper.selectIdsByExample()
.where(LmsSetupRecordDynamicSqlSupport.connectionId, isEqualTo(connectionId))
.build()
.execute();
if (find == null || find.isEmpty()) {
throw new NoResourceFoundException(EntityType.LMS_SETUP, "No LMSSetup with connection_id:" + connectionId);
}
return find.get(0);
});
}
@Override
@Transactional(readOnly = true)
public Result<Collection<LmsSetup>> all(final Long institutionId, final Boolean active) {

View file

@ -35,6 +35,8 @@ public interface ExamAdminService {
ProctoringAdminService getProctoringAdminService();
Result<Exam> applyPostCreationInitialization(Exam exam);
/** Get the exam domain object for the exam identifier (PK).
*
* @param examId the exam identifier
@ -165,178 +167,6 @@ public interface ExamAdminService {
Result<Exam> applyQuitPassword(Exam exam);
static void newExamFieldValidation(final POSTMapper postParams) {
noLMSFieldValidation(new Exam(postParams));
}
static Exam noLMSFieldValidation(final Exam exam) {
// This only applies to exams that has no LMS
if (exam.lmsSetupId != null) {
return exam;
}
final Collection<APIMessage> validationErrors = new ArrayList<>();
if (StringUtils.isBlank(exam.name)) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_NAME,
"exam:quizName:notNull"));
} else {
final int length = exam.name.length();
if (length < 3 || length > 255) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_NAME,
"exam:quizName:size:3:255:" + length));
}
}
if (StringUtils.isBlank(exam.getStartURL())) {
validationErrors.add(APIMessage.fieldValidationError(
QuizData.QUIZ_ATTR_START_URL,
"exam:quiz_start_url:notNull"));
} else {
try {
new URL(exam.getStartURL()).toURI();
} catch (final Exception e) {
validationErrors.add(APIMessage.fieldValidationError(
QuizData.QUIZ_ATTR_START_URL,
"exam:quiz_start_url:invalidURL"));
}
}
if (exam.startTime == null) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_START_TIME,
"exam:quizStartTime:notNull"));
} else if (exam.endTime != null) {
if (exam.startTime
.isAfter(exam.endTime)) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_END_TIME,
"exam:quizEndTime:endBeforeStart"));
}
}
if (!validationErrors.isEmpty()) {
throw new APIMessageException(validationErrors);
}
return exam;
}
/** Used to check threshold consistency for a given list of thresholds.
* Checks if all values are present (none null value)
* Checks if there are duplicates
* <p>
* If a check fails, the methods throws a APIMessageException with a FieldError to notify the caller
*
* @param thresholds List of Threshold */
public static void checkThresholdConsistency(final List<Threshold> thresholds) {
if (thresholds != null) {
final List<Threshold> emptyThresholds = thresholds.stream()
.filter(t -> t.getValue() == null || t.getColor() == null)
.collect(Collectors.toList());
if (!emptyThresholds.isEmpty()) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.EXAM.TYPE_NAME,
Domain.EXAM.ATTR_SUPPORTER,
"indicator:thresholds:thresholdEmpty")));
}
final Set<Double> values = thresholds.stream()
.map(t -> t.getValue())
.collect(Collectors.toSet());
if (values.size() != thresholds.size()) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.EXAM.TYPE_NAME,
Domain.EXAM.ATTR_SUPPORTER,
"indicator:thresholds:thresholdDuplicate")));
}
}
}
/** Used to check client group consistency for a given ClientGroup.
* Checks if correct entries for specific type
*
* If a check fails, the methods throws a APIMessageException with a FieldError to notify the caller
*
* @param clientGroup ClientGroup instance to check */
public static <T extends ClientGroupData> T checkClientGroupConsistency(final T clientGroup) {
final ClientGroupType type = clientGroup.getType();
if (type == null || type == ClientGroupType.NONE) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
Domain.CLIENT_GROUP.ATTR_TYPE,
"clientGroup:type:notNull")));
}
switch (type) {
case IP_V4_RANGE: {
checkIPRange(clientGroup.getIpRangeStart(), clientGroup.getIpRangeEnd());
break;
}
case CLIENT_OS: {
checkClientOS(clientGroup.getClientOS());
break;
}
default: {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
Domain.CLIENT_GROUP.ATTR_TYPE,
"clientGroup:type:typeInvalid")));
}
}
return clientGroup;
}
static void checkIPRange(final String ipRangeStart, final String ipRangeEnd) {
final long startIP = Utils.ipToLong(ipRangeStart);
if (StringUtils.isBlank(ipRangeStart) || startIP < 0) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroup.ATTR_IP_RANGE_START,
"clientGroup:ipRangeStart:invalidIP")));
}
final long endIP = Utils.ipToLong(ipRangeEnd);
if (StringUtils.isBlank(ipRangeEnd) || endIP < 0) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroup.ATTR_IP_RANGE_END,
"clientGroup:ipRangeEnd:invalidIP")));
}
if (endIP <= startIP) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroup.ATTR_IP_RANGE_START,
"clientGroup:ipRangeStart:invalidIPRange")),
APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroup.ATTR_IP_RANGE_END,
"clientGroup:ipRangeEnd:invalidIPRange")));
}
}
static void checkClientOS(final ClientOS clientOS) {
if (clientOS == null) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroupData.ATTR_CLIENT_OS,
"clientGroup:clientOS:notNull")));
}
}
Result<Exam> findExamByLmsIdentity(String courseId, String quizId, String identity);
}

View file

@ -0,0 +1,202 @@
/*
* 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.webservice.servicelayer.exam;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
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.exam.*;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.validation.FieldError;
public final class ExamUtils {
public static void newExamFieldValidation(final POSTMapper postParams) {
noLMSFieldValidation(new Exam(postParams));
}
public static Exam noLMSFieldValidation(final Exam exam) {
// This only applies to exams that has no LMS
if (exam.lmsSetupId != null) {
return exam;
}
final Collection<APIMessage> validationErrors = new ArrayList<>();
if (StringUtils.isBlank(exam.name)) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_NAME,
"exam:quizName:notNull"));
} else {
final int length = exam.name.length();
if (length < 3 || length > 255) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_NAME,
"exam:quizName:size:3:255:" + length));
}
}
if (StringUtils.isBlank(exam.getStartURL())) {
validationErrors.add(APIMessage.fieldValidationError(
QuizData.QUIZ_ATTR_START_URL,
"exam:quiz_start_url:notNull"));
} else {
try {
new URL(exam.getStartURL()).toURI();
} catch (final Exception e) {
validationErrors.add(APIMessage.fieldValidationError(
QuizData.QUIZ_ATTR_START_URL,
"exam:quiz_start_url:invalidURL"));
}
}
if (exam.startTime == null) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_START_TIME,
"exam:quizStartTime:notNull"));
} else if (exam.endTime != null) {
if (exam.startTime
.isAfter(exam.endTime)) {
validationErrors.add(APIMessage.fieldValidationError(
Domain.EXAM.ATTR_QUIZ_END_TIME,
"exam:quizEndTime:endBeforeStart"));
}
}
if (!validationErrors.isEmpty()) {
throw new APIMessage.APIMessageException(validationErrors);
}
return exam;
}
/** Used to check threshold consistency for a given list of thresholds.
* Checks if all values are present (none null value)
* Checks if there are duplicates
* <p>
* If a check fails, the methods throws a APIMessageException with a FieldError to notify the caller
*
* @param thresholds List of Threshold */
public static void checkThresholdConsistency(final List<Indicator.Threshold> thresholds) {
if (thresholds != null) {
final List<Indicator.Threshold> emptyThresholds = thresholds.stream()
.filter(t -> t.getValue() == null || t.getColor() == null)
.toList();
if (!emptyThresholds.isEmpty()) {
throw new APIMessage.APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.EXAM.TYPE_NAME,
Domain.EXAM.ATTR_SUPPORTER,
"indicator:thresholds:thresholdEmpty")));
}
final Set<Double> values = thresholds.stream()
.map(Indicator.Threshold::getValue)
.collect(Collectors.toSet());
if (values.size() != thresholds.size()) {
throw new APIMessage.APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.EXAM.TYPE_NAME,
Domain.EXAM.ATTR_SUPPORTER,
"indicator:thresholds:thresholdDuplicate")));
}
}
}
/** Used to check client group consistency for a given ClientGroup.
* Checks if correct entries for specific type
* <p>
* If a check fails, the methods throws a APIMessageException with a FieldError to notify the caller
*
* @param clientGroup ClientGroup instance to check */
public static <T extends ClientGroupData> T checkClientGroupConsistency(final T clientGroup) {
final ClientGroupData.ClientGroupType type = clientGroup.getType();
if (type == null || type == ClientGroupData.ClientGroupType.NONE) {
throw new APIMessage.APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
Domain.CLIENT_GROUP.ATTR_TYPE,
"clientGroup:type:notNull")));
}
switch (type) {
case IP_V4_RANGE: {
checkIPRange(clientGroup.getIpRangeStart(), clientGroup.getIpRangeEnd());
break;
}
case CLIENT_OS: {
checkClientOS(clientGroup.getClientOS());
break;
}
default: {
throw new APIMessage.APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
Domain.CLIENT_GROUP.ATTR_TYPE,
"clientGroup:type:typeInvalid")));
}
}
return clientGroup;
}
static void checkIPRange(final String ipRangeStart, final String ipRangeEnd) {
final long startIP = Utils.ipToLong(ipRangeStart);
if (StringUtils.isBlank(ipRangeStart) || startIP < 0) {
throw new APIMessage.APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroup.ATTR_IP_RANGE_START,
"clientGroup:ipRangeStart:invalidIP")));
}
final long endIP = Utils.ipToLong(ipRangeEnd);
if (StringUtils.isBlank(ipRangeEnd) || endIP < 0) {
throw new APIMessage.APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroup.ATTR_IP_RANGE_END,
"clientGroup:ipRangeEnd:invalidIP")));
}
if (endIP <= startIP) {
throw new APIMessage.APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroup.ATTR_IP_RANGE_START,
"clientGroup:ipRangeStart:invalidIPRange")),
APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroup.ATTR_IP_RANGE_END,
"clientGroup:ipRangeEnd:invalidIPRange")));
}
}
public static void checkClientOS(final ClientGroupData.ClientOS clientOS) {
if (clientOS == null) {
throw new APIMessage.APIMessageException(APIMessage.fieldValidationError(
new FieldError(
Domain.CLIENT_GROUP.TYPE_NAME,
ClientGroupData.ATTR_CLIENT_OS,
"clientGroup:clientOS:notNull")));
}
}
}

View file

@ -9,10 +9,14 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.util.encoders.Hex;
@ -41,16 +45,13 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.Configuration
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ProctoringAdminService;
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.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringService;
@Lazy
@ -71,6 +72,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
private final ExamConfigurationValueService examConfigurationValueService;
private final SEBRestrictionService sebRestrictionService;
private final ExamTemplateService examTemplateService;
protected ExamAdminServiceImpl(
final ExamDAO examDAO,
final ProctoringAdminService proctoringAdminService,
@ -80,6 +83,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
final LmsAPIService lmsAPIService,
final ExamConfigurationValueService examConfigurationValueService,
final SEBRestrictionService sebRestrictionService,
final ExamTemplateService examTemplateService,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.enabled:false}") boolean appSignatureKeyEnabled,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.numerical.threshold:2}") int defaultNumericalTrustThreshold) {
@ -93,6 +97,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
this.appSignatureKeyEnabled = appSignatureKeyEnabled;
this.defaultNumericalTrustThreshold = defaultNumericalTrustThreshold;
this.sebRestrictionService = sebRestrictionService;
this.examTemplateService = examTemplateService;
}
@Override
@ -100,6 +105,60 @@ public class ExamAdminServiceImpl implements ExamAdminService {
return this.proctoringAdminService;
}
@Override
public Result<Exam> applyPostCreationInitialization(final Exam exam) {
final List<APIMessage> errors = new ArrayList<>();
this.initAdditionalAttributes(exam)
.onErrorDo(error -> {
errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error));
return exam;
})
.flatMap(this.examTemplateService::addDefinedIndicators)
.onErrorDo(error -> {
errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_INDICATOR.of(error));
return exam;
})
.flatMap(this.examTemplateService::addDefinedClientGroups)
.onErrorDo(error -> {
errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CLIENT_GROUPS.of(error));
return exam;
})
.flatMap(this.examTemplateService::initAdditionalTemplateAttributes)
.onErrorDo(error -> {
errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error));
return exam;
})
.flatMap(this.examTemplateService::initExamConfiguration)
.onErrorDo(error -> {
if (error instanceof APIMessageException) {
errors.addAll(((APIMessageException) error).getAPIMessages());
} else {
errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CONFIG.of(error));
}
return exam;
})
.flatMap(this::applyAdditionalSEBRestrictions)
.onErrorDo(error -> {
errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_RESTRICTION.of(error));
return exam;
});
this.applyQuitPassword(exam);
if (!errors.isEmpty()) {
errors.add(0, APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_SETUP.of(
exam.getModelId(),
API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + exam.getModelId()));
log.warn("Exam successfully created but some initialization did go wrong: {}", errors);
throw new APIMessageException(errors);
} else {
return this.examDAO.byPK(exam.id);
}
}
@Override
public Result<Exam> examForPK(final Long examId) {
return this.examDAO.byPK(examId);
@ -343,6 +402,35 @@ public class ExamAdminServiceImpl implements ExamAdminService {
.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) {
return Result.tryCatch(() -> {

View file

@ -12,9 +12,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
public interface FullLmsIntegrationAPI {
Result<Void> createConnectionDetails();
Result<Void> updateConnectionDetails();
Result<Void> applyConnectionDetails(FullLmsIntegrationService.IntegrationData data);
Result<Void> deleteConnectionDetails();

View file

@ -14,11 +14,11 @@ import java.util.Map;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.util.Result;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
public interface FullLmsIntegrationService {
Result<LmsAPITemplate> getLmsAPITemplate(String lmsUUID);
Result<Void> refreshAccessToken(String lmsUUID);
Result<Void> applyFullLmsIntegration(Long lmsSetupId);
@ -45,4 +45,32 @@ public interface FullLmsIntegrationService {
String courseId,
String quizId,
OutputStream out);
@JsonIgnoreProperties(ignoreUnknown = true)
final class IntegrationData {
@JsonProperty("id")
public final String id;
@JsonProperty("name")
public final String name;
@JsonProperty("url")
public final String url;
@JsonProperty("access_token")
public final String access_token;
@JsonProperty("exam_templates")
public final Map<String, String> exam_templates;
public IntegrationData(
@JsonProperty("id") final String id,
@JsonProperty("name") final String name,
@JsonProperty("url") final String url,
@JsonProperty("access_token") final String access_token,
@JsonProperty("exam_templates") final Map<String, String> exam_templates) {
this.id = id;
this.name = name;
this.url = url;
this.access_token = access_token;
this.exam_templates = exam_templates;
}
}
}

View file

@ -68,7 +68,6 @@ public interface QuizLookupService {
* @param sortAttribute the sort attribute for the new Page
* @param pageNumber the number of the Page to build
* @param pageSize the size of the Page to build
* @param complete indicates if the quiz lookup that uses this page function has been completed yet
* @return A Page of QuizData extracted from a given list of QuizData */
static Function<LookupResult, Page<QuizData>> quizzesToPageFunction(
final String sortAttribute,

View file

@ -10,13 +10,16 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.io.OutputStream;
import java.util.Map;
import java.util.function.Function;
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.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -25,9 +28,16 @@ import org.springframework.stereotype.Service;
@Service
@WebServiceProfile
public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService {
@Override
public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsUUID) {
return Result.ofRuntimeError("TODO");
private final LmsSetupDAO lmsSetupDAO;
private final LmsAPIService lmsAPIService;
public FullLmsIntegrationServiceImpl(
final LmsSetupDAO lmsSetupDAO,
final LmsAPIService lmsAPIService) {
this.lmsSetupDAO = lmsSetupDAO;
this.lmsAPIService = lmsAPIService;
}
@Override
@ -58,9 +68,16 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final String examTemplateId,
final String quitPassword,
final String quitLink) {
return Result.ofRuntimeError("TODO");
return lmsSetupDAO.getLmsSetupIdByConnectionId(lmsUUID)
.flatMap(lmsAPIService::getLmsAPITemplate)
.map(findQuizData(courseId, quizId))
.map(createExam(examTemplateId, quitPassword, quitLink));
}
@Override
public Result<EntityKey> deleteExam(
final String lmsUUID,
@ -78,8 +95,30 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return Result.ofRuntimeError("TODO");
}
private Long findLMSSetup(final String lmsUUID) {
// TODO
return null;
private Function<LmsAPITemplate, QuizData> findQuizData(
final String courseId,
final String quizId) {
return LmsAPITemplate -> {
// TODO find quiz data for quizId and courseId on LMS
return null;
};
}
private Function<QuizData, Exam> createExam(
final String examTemplateId,
final String quitPassword,
final String quitLink) {
return quizData -> {
// TODO create and store Exam with DAO and apply all post processing needed for import
return null;
};
}
private Exam createAdHocSupporterAccount(Exam exam) {
// TODO create an ad hoc supporter account for this exam and apply it to the exam
return exam;
}
}

View file

@ -462,7 +462,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
}
@Override
public Result<Void> createConnectionDetails() {
public Result<Void> applyConnectionDetails(final FullLmsIntegrationService.IntegrationData data) {
if (this.lmsIntegrationAPI == null) {
return Result.ofError(
new UnsupportedOperationException("LMS Integration API Not Supported For: " + getType().name()));
@ -472,31 +472,13 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
log.debug("Create LMS connection details for LMSSetup: {}", lmsSetup());
}
return this.lmsAccessRequest.protectedRun(() -> this.lmsIntegrationAPI.createConnectionDetails()
return this.lmsAccessRequest.protectedRun(() -> this.lmsIntegrationAPI.applyConnectionDetails(data)
.onError(error -> log.error(
"Failed to run protected createConnectionDetails: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public Result<Void> updateConnectionDetails() {
if (this.lmsIntegrationAPI == null) {
return Result.ofError(
new UnsupportedOperationException("LMS Integration API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Update LMS connection details for LMSSetup: {}", lmsSetup());
}
return this.lmsAccessRequest.protectedRun(() -> this.lmsIntegrationAPI.updateConnectionDetails()
.onError(error -> log.error(
"Failed to run protected updateConnectionDetails: {}",
error.getMessage()))
.getOrThrow());
}
@Override
public Result<Void> deleteConnectionDetails() {
if (this.lmsIntegrationAPI == null) {
@ -514,4 +496,5 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
error.getMessage()))
.getOrThrow());
}
}

View file

@ -22,6 +22,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@ -421,12 +422,7 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
}
@Override
public Result<Void> createConnectionDetails() {
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<Void> updateConnectionDetails() {
public Result<Void> applyConnectionDetails(final FullLmsIntegrationService.IntegrationData data) {
return Result.ofRuntimeError("Not Supported");
}

View file

@ -10,17 +10,13 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
public class MockupFullIntegration implements FullLmsIntegrationAPI {
@Override
public Result<Void> createConnectionDetails() {
return Result.ofRuntimeError("TODO");
}
@Override
public Result<Void> updateConnectionDetails() {
public Result<Void> applyConnectionDetails(FullLmsIntegrationService.IntegrationData data) {
return Result.ofRuntimeError("TODO");
}

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
@ -27,12 +28,7 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
}
@Override
public Result<Void> createConnectionDetails() {
return Result.ofRuntimeError("TODO");
}
@Override
public Result<Void> updateConnectionDetails() {
public Result<Void> applyConnectionDetails(FullLmsIntegrationService.IntegrationData data) {
return Result.ofRuntimeError("TODO");
}

View file

@ -19,6 +19,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
@ -408,12 +409,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
}
@Override
public Result<Void> createConnectionDetails() {
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<Void> updateConnectionDetails() {
public Result<Void> applyConnectionDetails(final FullLmsIntegrationService.IntegrationData data) {
return Result.ofRuntimeError("Not Supported");
}

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamUtils;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -90,13 +91,13 @@ public class ClientGroupController extends EntityController<ClientGroup, ClientG
@Override
protected Result<ClientGroup> validForCreate(final ClientGroup entity) {
return super.validForCreate(entity)
.map(ExamAdminService::checkClientGroupConsistency);
.map(ExamUtils::checkClientGroupConsistency);
}
@Override
protected Result<ClientGroup> validForSave(final ClientGroup entity) {
return super.validForSave(entity)
.map(ExamAdminService::checkClientGroupConsistency);
.map(ExamUtils::checkClientGroupConsistency);
}
@Override

View file

@ -15,6 +15,7 @@ import java.util.stream.Collectors;
import javax.validation.Valid;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
@ -34,7 +35,6 @@ 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.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.model.Domain;
@ -67,7 +67,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
@ -88,7 +87,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
private final UserDAO userDAO;
private final ExamAdminService examAdminService;
private final RemoteProctoringRoomService remoteProctoringRoomService;
private final ExamTemplateService examTemplateService;
private final LmsAPIService lmsAPIService;
private final ExamSessionService examSessionService;
private final SEBRestrictionService sebRestrictionService;
@ -106,7 +104,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
final UserDAO userDAO,
final ExamAdminService examAdminService,
final RemoteProctoringRoomService remoteProctoringRoomService,
final ExamTemplateService examTemplateService,
final ExamSessionService examSessionService,
final SEBRestrictionService sebRestrictionService,
final SecurityKeyService securityKeyService,
@ -123,7 +120,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
this.userDAO = userDAO;
this.examAdminService = examAdminService;
this.remoteProctoringRoomService = remoteProctoringRoomService;
this.examTemplateService = examTemplateService;
this.lmsAPIService = lmsAPIService;
this.examSessionService = examSessionService;
this.sebRestrictionService = sebRestrictionService;
@ -596,7 +592,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
// NO LMS based exam is possible since v1.6
if (quizId == null) {
ExamAdminService.newExamFieldValidation(postParams);
ExamUtils.newExamFieldValidation(postParams);
return new Exam(postParams);
} else {
return this.lmsAPIService
@ -613,57 +609,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@Override
protected Result<Exam> notifyCreated(final Exam entity) {
final List<APIMessage> errors = new ArrayList<>();
this.examAdminService
.initAdditionalAttributes(entity)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error));
return entity;
})
.flatMap(this.examTemplateService::addDefinedIndicators)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_INDICATOR.of(error));
return entity;
})
.flatMap(this.examTemplateService::addDefinedClientGroups)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CLIENT_GROUPS.of(error));
return entity;
})
.flatMap(this.examTemplateService::initAdditionalTemplateAttributes)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error));
return entity;
})
.flatMap(this.examTemplateService::initExamConfiguration)
.onErrorDo(error -> {
if (error instanceof APIMessageException) {
errors.addAll(((APIMessageException) error).getAPIMessages());
} else {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CONFIG.of(error));
}
return entity;
})
.flatMap(this.examAdminService::applyAdditionalSEBRestrictions)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_RESTRICTION.of(error));
return entity;
});
this.examAdminService.applyQuitPassword(entity);
if (!errors.isEmpty()) {
errors.add(0, ErrorMessage.EXAM_IMPORT_ERROR_AUTO_SETUP.of(
entity.getModelId(),
API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + entity.getModelId()));
log.warn("Exam successfully created but some initialization did go wrong: {}", errors);
throw new APIMessageException(errors);
} else {
return this.examDAO.byPK(entity.id);
}
return examAdminService.applyPostCreationInitialization(entity);
}
@Override
@ -683,7 +629,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
protected Result<Exam> validForSave(final Exam entity) {
return super.validForSave(entity)
.map(this::checkExamSupporterRole)
.map(ExamAdminService::noLMSFieldValidation)
.map(ExamUtils::noLMSFieldValidation)
.map(this::checkQuitPasswordChange);
}

View file

@ -15,6 +15,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamUtils;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.dynamic.sql.SqlTable;
import org.slf4j.Logger;
@ -381,7 +382,7 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
null,
postMap.getLong(ClientGroupTemplate.ATTR_EXAM_TEMPLATE_ID),
postMap))
.map(ExamAdminService::checkClientGroupConsistency)
.map(ExamUtils::checkClientGroupConsistency)
.flatMap(this.examTemplateDAO::createNewClientGroupTemplate)
.flatMap(this.userActivityLogDAO::logCreate)
.getOrThrow();
@ -403,7 +404,7 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
this.checkModifyPrivilege(institutionId);
return this.beanValidationService
.validateBean(modifyData)
.map(ExamAdminService::checkClientGroupConsistency)
.map(ExamUtils::checkClientGroupConsistency)
.flatMap(this.examTemplateDAO::saveClientGroupTemplate)
.flatMap(this.userActivityLogDAO::logModify)
.getOrThrow();
@ -547,7 +548,7 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
}
private IndicatorTemplate checkIndicatorConsistency(final IndicatorTemplate indicatorTemplate) {
ExamAdminService.checkThresholdConsistency(indicatorTemplate.thresholds);
ExamUtils.checkThresholdConsistency(indicatorTemplate.thresholds);
return indicatorTemplate;
}

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamUtils;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -90,7 +91,7 @@ public class IndicatorController extends EntityController<Indicator, Indicator>
protected Result<Indicator> validForCreate(final Indicator entity) {
return super.validForCreate(entity)
.map(indicator -> {
ExamAdminService.checkThresholdConsistency(indicator.thresholds);
ExamUtils.checkThresholdConsistency(indicator.thresholds);
return indicator;
});
}
@ -99,7 +100,7 @@ public class IndicatorController extends EntityController<Indicator, Indicator>
protected Result<Indicator> validForSave(final Indicator entity) {
return super.validForSave(entity)
.map(indicator -> {
ExamAdminService.checkThresholdConsistency(indicator.thresholds);
ExamUtils.checkThresholdConsistency(indicator.thresholds);
return indicator;
});
}

View file

@ -60,8 +60,8 @@ public class QuizController {
/** This is called by Spring to initialize the WebDataBinder and is used here to
* initialize the default value binding for the institutionId request-parameter
* that has the current users insitutionId as default.
*
* that has the current users institutionId as default.
* <p>
* See also UserService.addUsersInstitutionDefaultPropertySupport */
@InitBinder
public void initBinder(final WebDataBinder binder) {