From 25265fdb2bde2923f9280a7d181b2ccfdcd3ada7 Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 15 Sep 2021 15:51:02 +0200 Subject: [PATCH] SEBSERV-162 create exam from template and tests --- .../ch/ethz/seb/sebserver/gbl/Constants.java | 6 +- .../gbl/model/exam/ExamConfigurationMap.java | 23 ++ .../ch/ethz/seb/sebserver/gbl/util/Utils.java | 8 + .../sebserver/gui/content/exam/ExamForm.java | 40 +++ .../gui/content/exam/ExamTemplateForm.java | 4 - .../gui/service/ResourceService.java | 13 + .../api/exam/GetExamTemplateNames.java | 42 +++ .../dao/AdditionalAttributesDAO.java | 22 ++ .../dao/impl/AdditionalAttributesDAOImpl.java | 51 ++- .../dao/impl/ExamTemplateDAOImpl.java | 3 +- .../servicelayer/exam/ExamAdminService.java | 16 +- .../exam/ExamTemplateService.java | 37 ++- .../exam/impl/ExamAdminServiceImpl.java | 108 ++---- .../exam/impl/ExamTemplateServiceImpl.java | 312 ++++++++++++++++++ .../api/ExamAdministrationController.java | 13 +- .../weblayer/api/ExamTemplateController.java | 22 +- .../config/application-ws.properties | 4 + src/main/resources/messages.properties | 3 + .../integration/UseCasesIntegrationTest.java | 122 ++++++- 19 files changed, 721 insertions(+), 128 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetExamTemplateNames.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java index 21a9318c..6e9d02d9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java @@ -84,13 +84,15 @@ public final class Constants { public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; public static final String TIME_ZONE_OFFSET_TAIL_FORMAT = "|ZZ"; - //public static final String DEFAULT_DISPLAY_DATE_TIME_FORMAT = "MM-dd-yyyy HH:mm:ss"; - public static final String DEFAULT_DISPLAY_DATE_FORMAT = "MM-dd-yyyy"; + public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; public static final DateTimeFormatter STANDARD_DATE_TIME_FORMATTER = DateTimeFormat .forPattern(DEFAULT_DATE_TIME_FORMAT) .withZoneUTC(); + public static final DateTimeFormatter STANDARD_DATE_FORMATTER = DateTimeFormat + .forPattern(DEFAULT_DATE_FORMAT) + .withZoneUTC(); public static final String XML_VERSION_HEADER = ""; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamConfigurationMap.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamConfigurationMap.java index 69040a27..c91154b4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamConfigurationMap.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamConfigurationMap.java @@ -136,6 +136,29 @@ public final class ExamConfigurationMap implements GrantEntity { this.configStatus = postParams.getEnum(Domain.CONFIGURATION_NODE.ATTR_STATUS, ConfigurationStatus.class); } + public ExamConfigurationMap( + final Long institutionId, + final Long examId, + final Long configurationNodeId, + final String userNames) { + + this.id = null; + this.institutionId = institutionId; + this.examId = examId; + this.examName = null; + this.examDescription = null; + this.examStartTime = null; + this.examType = null; + this.configurationNodeId = configurationNodeId; + this.userNames = userNames; + this.encryptSecret = null; + this.confirmEncryptSecret = null; + + this.configName = null; + this.configDescription = null; + this.configStatus = null; + } + @Override public EntityType entityType() { return EntityType.EXAM_CONFIGURATION_MAP; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java index 05b4e64c..f3fae144 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java @@ -712,4 +712,12 @@ public final class Utils { return false; // Either timeout or unreachable or failed DNS lookup. } } + + public static String replaceAll(final String template, final Map values) { + String result = template; + for (final Map.Entry e : values.entrySet()) { + result = result.replace(e.getKey(), e.getValue()); + } + return result; + } } 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 97fc66cd..382e0632 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 @@ -33,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; +import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; @@ -41,6 +42,7 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; +import ch.ethz.seb.sebserver.gui.form.Form; import ch.ethz.seb.sebserver.gui.form.FormBuilder; import ch.ethz.seb.sebserver.gui.form.FormHandle; import ch.ethz.seb.sebserver.gui.service.ResourceService; @@ -58,6 +60,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamTemplate; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup; @@ -104,6 +107,10 @@ public class ExamForm implements TemplateComposer { new LocTextKey("sebserver.exam.form.quizurl"); private static final LocTextKey FORM_LMSSETUP_TEXT_KEY = new LocTextKey("sebserver.exam.form.lmssetup"); + private static final LocTextKey FORM_EXAM_TEMPLATE_TEXT_KEY = + new LocTextKey("sebserver.exam.form.examTemplate"); + private static final LocTextKey FORM_EXAM_TEMPLATE_ERROR = + new LocTextKey("sebserver.exam.form.examTemplate.error"); private final static LocTextKey CONSISTENCY_MESSAGE_TITLE = new LocTextKey("sebserver.exam.consistency.title"); @@ -329,6 +336,17 @@ public class ExamForm implements TemplateComposer { .withInputSpan(4) .withEmptyCellSeparation(false)) + .addField(FormBuilder.singleSelection( + Domain.EXAM.ATTR_EXAM_TEMPLATE_ID, + FORM_EXAM_TEMPLATE_TEXT_KEY, + (exam.examTemplateId == null) ? null : String.valueOf(exam.examTemplateId), + this.resourceService::examTemplateResources) + .withSelectionListener(form -> this.processTemplateSelection(form, formContext)) + .withLabelSpan(2) + .withInputSpan(4) + .withEmptyCellSpan(2) + .mandatory(!readonly)) + .addField(FormBuilder.singleSelection( Domain.EXAM.ATTR_TYPE, FORM_TYPE_TEXT_KEY, @@ -448,6 +466,28 @@ public class ExamForm implements TemplateComposer { } } + private void processTemplateSelection(final Form form, final PageContext context) { + try { + final String templateId = form.getFieldValue(Domain.EXAM.ATTR_EXAM_TEMPLATE_ID); + if (StringUtils.isNotBlank(templateId)) { + final ExamTemplate examTemplate = this.pageService.getRestService().getBuilder(GetExamTemplate.class) + .withURIVariable(API.PARAM_MODEL_ID, templateId) + .call() + .getOrThrow(); + + form.setFieldValue(Domain.EXAM.ATTR_TYPE, examTemplate.examType.name()); + form.setFieldValue( + Domain.EXAM.ATTR_SUPPORTER, + StringUtils.join(examTemplate.supporter, Constants.LIST_SEPARATOR)); + } else { + form.setFieldValue(Domain.EXAM.ATTR_TYPE, Exam.ExamType.UNDEFINED.name()); + form.setFieldValue(Domain.EXAM.ATTR_SUPPORTER, null); + } + } catch (final Exception e) { + context.notifyError(FORM_EXAM_TEMPLATE_ERROR, e); + } + } + private PageAction importExam( final PageAction action, final FormHandle formHandle, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamTemplateForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamTemplateForm.java index 92e51cd5..a047df9d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamTemplateForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamTemplateForm.java @@ -12,8 +12,6 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; import org.eclipse.swt.widgets.Composite; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -55,8 +53,6 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; @GuiProfile public class ExamTemplateForm implements TemplateComposer { - private static final Logger log = LoggerFactory.getLogger(ExamTemplateForm.class); - public static final LocTextKey TITLE_TEXT_KEY = new LocTextKey("sebserver.examtemplate.form.title"); public static final LocTextKey TITLE_NEW_TEXT_KEY = 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 b6fbfd46..08e7bdad 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 @@ -75,6 +75,7 @@ import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamNames; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamTemplateNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExams; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupNames; @@ -786,4 +787,16 @@ public class ResourceService { .collect(Collectors.toList()); } + public List> examTemplateResources() { + return Stream.concat( + Stream.of(new EntityName("", EntityType.EXAM_TEMPLATE, "")), + this.restService.getBuilder(GetExamTemplateNames.class) + .call() + .onError(error -> log.warn("Failed to get exam template names: {}", error.getMessage())) + .getOr(Collections.emptyList()) + .stream()) + .map(entityName -> new Tuple<>(entityName.modelId, entityName.name)) + .collect(Collectors.toList()); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetExamTemplateNames.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetExamTemplateNames.java new file mode 100644 index 00000000..c282615b --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetExamTemplateNames.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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.gui.service.remote.webservice.api.exam; + +import java.util.List; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.EntityName; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetExamTemplateNames extends RestCall> { + + public GetExamTemplateNames() { + super(new TypeKey<>( + CallType.GET_NAMES, + EntityType.EXAM_TEMPLATE, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_TEMPLATE_ENDPOINT + API.NAMES_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java index 2947f30e..1d73b34b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java @@ -9,6 +9,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -55,6 +57,26 @@ public interface AdditionalAttributesDAO { String name, String value); + /** Use this to save an additional attributes for a specific entity. + * If an additional attribute with specified name already exists for the specified entity + * this updates just the value for this additional attribute. Otherwise create a new instance + * of additional attribute with the given data + * + * @param type the entity type + * @param entityId the entity identifier (primary key) + * @param attributes Map of attributes to save for */ + default Result> saveAdditionalAttributes( + final EntityType type, + final Long entityId, + final Map attributes) { + + return Result.tryCatch(() -> attributes.entrySet() + .stream() + .map(attr -> saveAdditionalAttribute(type, entityId, attr.getKey(), attr.getValue())) + .flatMap(Result::onErrorLogAndSkip) + .collect(Collectors.toList())); + } + /** Use this to delete an additional attribute by identifier (primary-key) * * @param id identifier (primary-key) */ diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/AdditionalAttributesDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/AdditionalAttributesDAOImpl.java index f191211d..c963e9a2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/AdditionalAttributesDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/AdditionalAttributesDAOImpl.java @@ -91,11 +91,17 @@ public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO { final String value) { return Result.tryCatch(() -> { + if (value == null) { throw new IllegalArgumentException( "value cannot be null. Use delete to delete an additional attribute"); } + if (log.isDebugEnabled()) { + log.debug("Save additional attribute. Type: {}, entity: {}, name: {}, value: {}", + type, entityId, name, value); + } + final Optional id = this.additionalAttributeRecordMapper .selectIdsByExample() .where( @@ -150,26 +156,43 @@ public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO { @Override @Transactional public void delete(final EntityType type, final Long entityId, final String name) { - this.additionalAttributeRecordMapper - .deleteByExample() - .where( - AdditionalAttributeRecordDynamicSqlSupport.entityType, - SqlBuilder.isEqualTo(type.name())) - .and( - AdditionalAttributeRecordDynamicSqlSupport.entityId, - SqlBuilder.isEqualTo(entityId)) - .and( - AdditionalAttributeRecordDynamicSqlSupport.name, - SqlBuilder.isEqualTo(name)) - .build() - .execute(); + try { + + if (log.isDebugEnabled()) { + log.debug("Delete additional attribute. Type: {}, entity: {}, name: {}", + type, entityId, name); + } + + this.additionalAttributeRecordMapper + .deleteByExample() + .where( + AdditionalAttributeRecordDynamicSqlSupport.entityType, + SqlBuilder.isEqualTo(type.name())) + .and( + AdditionalAttributeRecordDynamicSqlSupport.entityId, + SqlBuilder.isEqualTo(entityId)) + .and( + AdditionalAttributeRecordDynamicSqlSupport.name, + SqlBuilder.isEqualTo(name)) + .build() + .execute(); + } catch (final Exception e) { + log.error("Failed to delete additional attribute: Type: {}, entity: {}, name: {}", + type, entityId, name, e); + } } @Override @Transactional public void deleteAll(final EntityType type, final Long entityId) { try { + + if (log.isDebugEnabled()) { + log.debug("Delete all additional attributes. Type: {}, entity: {}", + type, entityId); + } + this.additionalAttributeRecordMapper .deleteByExample() .where( @@ -181,7 +204,7 @@ public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO { .build() .execute(); } catch (final Exception e) { - log.warn("Failed to delete all additional attributes for: {} cause: {}", entityId, e.getMessage()); + log.error("Failed to delete all additional attributes for: {} cause: {}", entityId, e); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamTemplateDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamTemplateDAOImpl.java index 23e72f4a..7d369aca 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamTemplateDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamTemplateDAOImpl.java @@ -329,7 +329,8 @@ public class ExamTemplateDAOImpl implements ExamTemplateDAO { final Long count = this.examTemplateRecordMapper .countByExample() .where(ExamTemplateRecordDynamicSqlSupport.name, isEqualTo(examTemplate.name)) - .and(ExamTemplateRecordDynamicSqlSupport.id, isNotEqualToWhenPresent(examTemplate.institutionId)) + .and(ExamTemplateRecordDynamicSqlSupport.institutionId, isEqualTo(examTemplate.institutionId)) + .and(ExamTemplateRecordDynamicSqlSupport.id, isNotEqualToWhenPresent(examTemplate.id)) .build() .execute(); 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 51314f64..2856f162 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 @@ -16,23 +16,17 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringServi public interface ExamAdminService { - /** Adds a default indicator that is defined by configuration to a given exam. + /** Saves additional attributes for the exam that are specific to a type of LMS * - * @param exam The Exam to add the default indicator - * @return Result refer to the Exam with added default indicator or to an error if happened */ - Result addDefaultIndicator(Exam exam); - - /** Saves additional attributes for a specified Exam on creation or on update. - * - * @param exam The Exam to add the default indicator - * @return Result refer */ - Result saveAdditionalAttributes(Exam exam); + * @param exam The Exam to add the LMS specific attributes + * @return Result refer to the created exam or to an error when happened */ + Result saveLMSAttributes(Exam exam); /** Applies all additional SEB restriction attributes that are defined by the * type of the LMS of a given Exam to this given Exam. * * @param exam the Exam to apply all additional SEB restriction attributes - * @return the Exam */ + * @return Result refer to the created exam or to an error when happened */ Result applyAdditionalSEBRestrictions(Exam exam); /** Indicates whether a specific exam is been restricted with SEB restriction feature on the LMS or not. diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java index 205e1622..41f45fdf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java @@ -1,6 +1,6 @@ /* * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) - * + * * 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/. @@ -8,6 +8,41 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.util.Result; + public interface ExamTemplateService { + String VAR_START_DATE = "__startDate__"; + String VAR_CURRENT_DATE = "__currentDate__"; + String VAR_EXAM_NAME = "__examName__"; + String VAR_EXAM_TEMPLATE_NAME = "__examTemplateName__"; + + String DEFAULT_EXAM_CONFIG_NAME_TEMPLATE = VAR_START_DATE + " " + VAR_EXAM_NAME; + String DEFAULT_EXAM_CONFIG_DESC_TEMPLATE = + "This has automatically been created from the exam template: " + + VAR_EXAM_TEMPLATE_NAME + " at: " + + VAR_CURRENT_DATE; + + /** Adds the indicators that are defined by a exam template or + * a default indicator that is defined by configuration to a given exam. + * + * @param exam The Exam to add the default indicator + * @return Result refer to the Exam with added default indicator or to an error if happened */ + Result addDefinedIndicators(Exam exam); + + /** Initializes additional attributes for a specified Exam on creation. + * + * @param exam The Exam to add the default indicator + * @return Result refer to the created exam or to an error when happened */ + Result initAdditionalAttributes(Exam exam); + + /** Initializes a pre defined exam configuration. The configuration template to create a exam configuration + * is defined by a given linked exam template. This is used to create the exam configuration and automatically + * link it to the newly created exam + * + * @param exam The Exam to create and add new exam configuration + * @return Result refer to the created exam or to an error when happened */ + Result initExamConfiguration(Exam exam); + } 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 7c27aab8..5a2515b8 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 @@ -9,7 +9,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; import java.util.Arrays; -import java.util.Collection; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -21,19 +20,13 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.fasterxml.jackson.core.type.TypeReference; - import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; -import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; -import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature; @@ -47,7 +40,6 @@ import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; 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.IndicatorDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; @@ -64,80 +56,40 @@ public class ExamAdminServiceImpl implements ExamAdminService { private static final Logger log = LoggerFactory.getLogger(ExamAdminServiceImpl.class); private final ExamDAO examDAO; - private final IndicatorDAO indicatorDAO; private final AdditionalAttributesDAO additionalAttributesDAO; private final LmsAPIService lmsAPIService; - private final JSONMapper jsonMapper; private final Cryptor cryptor; private final ExamProctoringServiceFactory examProctoringServiceFactory; private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; - private final String defaultIndicatorName; - private final String defaultIndicatorType; - private final String defaultIndicatorColor; - private final String defaultIndicatorThresholds; - protected ExamAdminServiceImpl( final ExamDAO examDAO, - final IndicatorDAO indicatorDAO, final AdditionalAttributesDAO additionalAttributesDAO, final LmsAPIService lmsAPIService, - final JSONMapper jsonMapper, final Cryptor cryptor, final ExamProctoringServiceFactory examProctoringServiceFactory, - final RemoteProctoringRoomDAO remoteProctoringRoomDAO, - @Value("${sebserver.webservice.api.exam.indicator.name:Ping}") final String defaultIndicatorName, - @Value("${sebserver.webservice.api.exam.indicator.type:LAST_PING}") final String defaultIndicatorType, - @Value("${sebserver.webservice.api.exam.indicator.color:b4b4b4}") final String defaultIndicatorColor, - @Value("${sebserver.webservice.api.exam.indicator.thresholds:[{\"value\":2000.0,\"color\":\"22b14c\"},{\"value\":5000.0,\"color\":\"ff7e00\"},{\"value\":10000.0,\"color\":\"ed1c24\"}]}") final String defaultIndicatorThresholds) { + final RemoteProctoringRoomDAO remoteProctoringRoomDAO) { this.examDAO = examDAO; - this.indicatorDAO = indicatorDAO; this.additionalAttributesDAO = additionalAttributesDAO; this.lmsAPIService = lmsAPIService; - this.jsonMapper = jsonMapper; this.cryptor = cryptor; this.examProctoringServiceFactory = examProctoringServiceFactory; this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; - this.defaultIndicatorName = defaultIndicatorName; - this.defaultIndicatorType = defaultIndicatorType; - this.defaultIndicatorColor = defaultIndicatorColor; - this.defaultIndicatorThresholds = defaultIndicatorThresholds; - } - - @Override - public Result addDefaultIndicator(final Exam exam) { - return Result.tryCatch(() -> { - - final Collection thresholds = this.jsonMapper.readValue( - this.defaultIndicatorThresholds, - new TypeReference>() { - }); - - final Indicator indicator = new Indicator( - null, - exam.id, - this.defaultIndicatorName, - IndicatorType.valueOf(this.defaultIndicatorType), - this.defaultIndicatorColor, - null, - null, - thresholds); - - this.indicatorDAO.createNew(indicator) - .getOrThrow(); - - return this.examDAO - .byPK(exam.id) - .getOrThrow(); - }); } @Override public Result applyAdditionalSEBRestrictions(final Exam exam) { return Result.tryCatch(() -> { - final LmsSetup lmsSetup = this.lmsAPIService.getLmsSetup(exam.lmsSetupId) + + if (log.isDebugEnabled()) { + log.debug("Apply additional SEB restrictions for exam: {}", + exam.externalId); + } + + final LmsSetup lmsSetup = this.lmsAPIService + .getLmsSetup(exam.lmsSetupId) .getOrThrow(); if (lmsSetup.lmsType == LmsType.OPEN_EDX) { @@ -161,30 +113,10 @@ public class ExamAdminServiceImpl implements ExamAdminService { } @Override - public Result saveAdditionalAttributes(final Exam exam) { + public Result saveLMSAttributes(final Exam exam) { return saveAdditionalAttributesForMoodleExams(exam); } - private Result saveAdditionalAttributesForMoodleExams(final Exam exam) { - return Result.tryCatch(() -> { - final LmsAPITemplate lmsTemplate = this.lmsAPIService - .getLmsAPITemplate(exam.lmsSetupId) - .getOrThrow(); - - if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE) { - lmsTemplate.getQuiz(exam.externalId) - .flatMap(quizData -> this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - QuizData.QUIZ_ATTR_NAME, - quizData.name)) - .getOrThrow(); - } - - return exam; - }); - } - @Override public Result isRestricted(final Exam exam) { if (exam == null) { @@ -386,4 +318,24 @@ public class ExamAdminServiceImpl implements ExamAdminService { } } + private Result saveAdditionalAttributesForMoodleExams(final Exam exam) { + return Result.tryCatch(() -> { + final LmsAPITemplate lmsTemplate = this.lmsAPIService + .getLmsAPITemplate(exam.lmsSetupId) + .getOrThrow(); + + if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE) { + lmsTemplate.getQuiz(exam.externalId) + .flatMap(quizData -> this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + exam.id, + QuizData.QUIZ_ATTR_NAME, + quizData.name)) + .getOrThrow(); + } + + return exam; + }); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java new file mode 100644 index 00000000..57fdadf1 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java @@ -0,0 +1,312 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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.impl; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; +import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; +import ch.ethz.seb.sebserver.gbl.model.exam.IndicatorTemplate; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType; +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.ExamTemplateDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; +import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService; + +@Lazy +@Service +@WebServiceProfile +public class ExamTemplateServiceImpl implements ExamTemplateService { + + private static final Logger log = LoggerFactory.getLogger(ExamTemplateServiceImpl.class); + + private final AdditionalAttributesDAO additionalAttributesDAO; + private final ExamAdminService examAdminService; + private final ExamTemplateDAO examTemplateDAO; + private final ConfigurationNodeDAO configurationNodeDAO; + private final ExamConfigurationMapDAO examConfigurationMapDAO; + private final IndicatorDAO indicatorDAO; + private final JSONMapper jsonMapper; + + private final String defaultIndicatorName; + private final String defaultIndicatorType; + private final String defaultIndicatorColor; + private final String defaultIndicatorThresholds; + private final String defaultExamConfigNameTemplate; + private final String defaultExamConfigDescTemplate; + + public ExamTemplateServiceImpl( + final AdditionalAttributesDAO additionalAttributesDAO, + final ExamAdminService examAdminService, + final ExamTemplateDAO examTemplateDAO, + final ConfigurationNodeDAO configurationNodeDAO, + final ExamConfigurationMapDAO examConfigurationMapDAO, + final IndicatorDAO indicatorDAO, + final JSONMapper jsonMapper, + + @Value("${sebserver.webservice.api.exam.indicator.name:Ping}") final String defaultIndicatorName, + @Value("${sebserver.webservice.api.exam.indicator.type:LAST_PING}") final String defaultIndicatorType, + @Value("${sebserver.webservice.api.exam.indicator.color:b4b4b4}") final String defaultIndicatorColor, + @Value("${sebserver.webservice.api.exam.indicator.thresholds:[{\"value\":2000.0,\"color\":\"22b14c\"},{\"value\":5000.0,\"color\":\"ff7e00\"},{\"value\":10000.0,\"color\":\"ed1c24\"}]}") final String defaultIndicatorThresholds, + @Value("${sebserver.webservice.configtemplate.examconfig.default.name:") final String defaultExamConfigNameTemplate, + @Value("${sebserver.webservice.configtemplate.examconfig.default.description:}") final String defaultExamConfigDescTemplate) { + + this.examTemplateDAO = examTemplateDAO; + this.configurationNodeDAO = configurationNodeDAO; + this.examConfigurationMapDAO = examConfigurationMapDAO; + this.additionalAttributesDAO = additionalAttributesDAO; + this.examAdminService = examAdminService; + this.indicatorDAO = indicatorDAO; + this.jsonMapper = jsonMapper; + + this.defaultIndicatorName = defaultIndicatorName; + this.defaultIndicatorType = defaultIndicatorType; + this.defaultIndicatorColor = defaultIndicatorColor; + this.defaultIndicatorThresholds = defaultIndicatorThresholds; + + this.defaultExamConfigNameTemplate = (StringUtils.isNotBlank(defaultExamConfigDescTemplate)) + ? defaultExamConfigNameTemplate + : DEFAULT_EXAM_CONFIG_NAME_TEMPLATE; + this.defaultExamConfigDescTemplate = (StringUtils.isNotBlank(defaultExamConfigDescTemplate)) + ? defaultExamConfigDescTemplate + : DEFAULT_EXAM_CONFIG_DESC_TEMPLATE; + } + + @Override + public Result addDefinedIndicators(final Exam exam) { + if (exam.examTemplateId != null) { + return addIndicatorsFromTemplate(exam); + } else { + return addDefaultIndicator(exam); + } + } + + @Override + public Result initAdditionalAttributes(final Exam exam) { + return this.examAdminService.saveLMSAttributes(exam) + .map(_exam -> { + + if (exam.examTemplateId != null) { + + if (log.isDebugEnabled()) { + log.debug("Init exam: {} with additional attributes form exam template: {}", + exam.externalId, + exam.examTemplateId); + } + + final ExamTemplate examTemplate = this.examTemplateDAO + .byPK(exam.examTemplateId) + .onError(error -> log.warn("No exam template found for id: {}", + exam.examTemplateId, + error.getMessage())) + .getOr(null); + + if (examTemplate == null) { + return exam; + } + + if (examTemplate.examAttributes != null && !examTemplate.examAttributes.isEmpty()) { + this.additionalAttributesDAO.saveAdditionalAttributes( + EntityType.EXAM, + exam.getId(), + examTemplate.examAttributes); + } + } + return _exam; + }); + } + + @Override + public Result initExamConfiguration(final Exam exam) { + return Result.tryCatch(() -> { + + if (exam.examTemplateId != null) { + + if (log.isDebugEnabled()) { + log.debug("Init exam: {} from template: {}", exam.externalId, exam.examTemplateId); + } + + final ExamTemplate examTemplate = this.examTemplateDAO + .byPK(exam.examTemplateId) + .onError(error -> log.warn("No exam template found for id: {}", + exam.examTemplateId, + error.getMessage())) + .getOr(null); + + if (examTemplate == null) { + return exam; + } + + if (examTemplate.configTemplateId != null) { + + // create new exam configuration for the exam + final ConfigurationNode examConfig = this.configurationNodeDAO + .createNew(new ConfigurationNode( + null, + exam.institutionId, + examTemplate.configTemplateId, + replaceVars(this.defaultExamConfigNameTemplate, exam, examTemplate), + replaceVars(this.defaultExamConfigDescTemplate, exam, examTemplate), + ConfigurationType.EXAM_CONFIG, + exam.owner, + ConfigurationStatus.CONSTRUCTION)) + .onError(error -> log.error( + "Failed to create exam configuration for exam: {} from template: {}", + exam, + examTemplate, + error)) + .getOrThrow(); + + // map the exam configuration to the exam + this.examConfigurationMapDAO.createNew(new ExamConfigurationMap( + exam.institutionId, + exam.id, + examConfig.id, + null)) + .onError(error -> log.error( + "Failed to create exam configuration mapping for exam: {} for exam config: {}", + exam, + examConfig, + error)) + .getOrThrow(); + + } + } else { + if (log.isDebugEnabled()) { + log.debug("Not exam template defined for exam: {}", exam.externalId); + } + } + + return exam; + }); + } + + private Result addIndicatorsFromTemplate(final Exam exam) { + return Result.tryCatch(() -> { + + if (exam.examTemplateId != null) { + + if (log.isDebugEnabled()) { + log.debug("Init exam: {} from template: {}", exam.externalId, exam.examTemplateId); + } + + final ExamTemplate examTemplate = this.examTemplateDAO + .byPK(exam.examTemplateId) + .onError(error -> log.warn("No exam template found for id: {}", + exam.examTemplateId, + error.getMessage())) + .getOr(null); + + if (examTemplate == null) { + return exam; + } + + examTemplate.indicatorTemplates + .forEach(it -> createIndicatorFromTemplate(it, exam)); + + } + + return exam; + }); + } + + private void createIndicatorFromTemplate(final IndicatorTemplate template, final Exam exam) { + try { + + this.indicatorDAO.createNew( + new Indicator( + null, + exam.id, + template.name, + template.type, + template.defaultColor, + template.defaultIcon, + template.tags, + template.thresholds)) + .getOrThrow(); + + } catch (final Exception e) { + log.error("Failed to automatically create indicator from template: {} for exam: {}", + template, + exam, + e); + } + } + + private Result addDefaultIndicator(final Exam exam) { + return Result.tryCatch(() -> { + + if (log.isDebugEnabled()) { + log.debug("Init default indicator for exam: {}", exam.externalId); + } + + final Collection thresholds = this.jsonMapper.readValue( + this.defaultIndicatorThresholds, + new TypeReference>() { + }); + + this.indicatorDAO.createNew( + new Indicator( + null, + exam.id, + this.defaultIndicatorName, + IndicatorType.valueOf(this.defaultIndicatorType), + this.defaultIndicatorColor, + null, + null, + thresholds)) + .getOrThrow(); + + return exam; + }); + } + + private String replaceVars(final String template, final Exam exam, final ExamTemplate examTemplate) { + final String currentDate = DateTime.now(DateTimeZone.UTC).toString(Constants.STANDARD_DATE_FORMATTER); + final Map vars = new HashMap<>(); + vars.put(VAR_CURRENT_DATE, currentDate); + vars.put( + VAR_START_DATE, + (exam.startTime != null) + ? exam.startTime.toString(Constants.STANDARD_DATE_FORMATTER) + : currentDate); + vars.put(VAR_EXAM_NAME, exam.name); + vars.put(VAR_EXAM_TEMPLATE_NAME, examTemplate.name); + + return Utils.replaceAll(template, vars); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 8324b03e..ada43c21 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -70,6 +70,7 @@ 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.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService; @@ -86,6 +87,7 @@ public class ExamAdministrationController extends EntityController { private final ExamDAO examDAO; private final UserDAO userDAO; private final ExamAdminService examAdminService; + private final ExamTemplateService examTemplateService; private final LmsAPIService lmsAPIService; private final ExamConfigService sebExamConfigService; private final ExamSessionService examSessionService; @@ -101,6 +103,7 @@ public class ExamAdministrationController extends EntityController { final LmsAPIService lmsAPIService, final UserDAO userDAO, final ExamAdminService examAdminService, + final ExamTemplateService examTemplateService, final ExamConfigService sebExamConfigService, final ExamSessionService examSessionService, final SEBRestrictionService sebRestrictionService) { @@ -115,6 +118,7 @@ public class ExamAdministrationController extends EntityController { this.examDAO = examDAO; this.userDAO = userDAO; this.examAdminService = examAdminService; + this.examTemplateService = examTemplateService; this.lmsAPIService = lmsAPIService; this.sebExamConfigService = sebExamConfigService; this.examSessionService = examSessionService; @@ -459,9 +463,10 @@ public class ExamAdministrationController extends EntityController { @Override protected Result notifyCreated(final Exam entity) { - return this.examAdminService - .addDefaultIndicator(entity) - .flatMap(this.examAdminService::saveAdditionalAttributes) + return this.examTemplateService + .addDefinedIndicators(entity) + .flatMap(this.examTemplateService::initAdditionalAttributes) + .flatMap(this.examTemplateService::initExamConfiguration) .flatMap(this.examAdminService::applyAdditionalSEBRestrictions); } @@ -470,7 +475,7 @@ public class ExamAdministrationController extends EntityController { return Result.tryCatch(() -> { this.examSessionService.flushCache(entity); return entity; - }).flatMap(this.examAdminService::saveAdditionalAttributes); + }).flatMap(this.examAdminService::saveLMSAttributes); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamTemplateController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamTemplateController.java index 91d9da7e..32d5b0d0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamTemplateController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamTemplateController.java @@ -133,15 +133,15 @@ public class ExamTemplateController extends EntityController t.indicatorTemplates + .stream() + .filter(i -> modelId.equals(i.getModelId())) + .findFirst() + .orElseThrow(() -> new ResourceNotFoundException(EntityType.INDICATOR, parentModelId))) .getOrThrow(); - return examTemplate.indicatorTemplates - .stream() - .filter(i -> modelId.equals(i.getModelId())) - .findFirst() - .orElseThrow(() -> new ResourceNotFoundException(EntityType.INDICATOR, parentModelId)); } @RequestMapping( @@ -220,7 +220,15 @@ public class ExamTemplateController extends EntityController { if (modelId.equals(i.getModelId())) { - return modifyData; + return new IndicatorTemplate( + modifyData.id, + modifyData.examTemplateId, + modifyData.name, + (modifyData.type != null) ? modifyData.type : i.type, + (modifyData.defaultColor != null) ? modifyData.defaultColor : i.defaultColor, + (modifyData.defaultIcon != null) ? modifyData.defaultIcon : i.defaultIcon, + (modifyData.tags != null) ? modifyData.tags : i.tags, + (modifyData.thresholds != null) ? modifyData.thresholds : i.thresholds); } else { return i; } diff --git a/src/main/resources/config/application-ws.properties b/src/main/resources/config/application-ws.properties index 9e6c6799..9f4eb807 100644 --- a/src/main/resources/config/application-ws.properties +++ b/src/main/resources/config/application-ws.properties @@ -71,3 +71,7 @@ sebserver.webservice.proctoring.resetBroadcastOnLeav=true sebserver.webservice.proctoring.zoom.enableWaitingRoom=false sebserver.webservice.proctoring.zoom.sendRejoinForCollectingRoom=true +sebserver.webservice.configtemplate.examconfig.default.name=__startDate__ __examName__ +sebserver.webservice.configtemplate.examconfig.default.description=This has automatically been created from the exam template: __examTemplateName__ at: __currentDate__ + + diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 2d12ec4a..db2251da 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -485,6 +485,9 @@ sebserver.exam.form.type=Exam Type sebserver.exam.form.type.tooltip=The type of the exam.

This has only descriptive character for now and can be used to categorise exams within a type sebserver.exam.form.supporter=Exam Supporter sebserver.exam.form.supporter.tooltip=A list of users that are allowed to support this exam

To add a user in edit mode click into the field on the right-hand side and start typing the first letters of the username.
A filtered choice will drop down. Select a specific username in the dropdown list to add the user to the list.
To remove a user from the list, just double-click the username on the list. +sebserver.exam.form.examTemplate=Exam Template +sebserver.exam.form.examTemplate.tooltip=Select an exam template to automatically create the exam, exam configuration and indicators defined by the template. +sebserver.exam.form.examTemplate.error=Failed to set type and supporter form template.
Please make sure you have selected the right type and supporter or tray again. sebserver.exam.form.export.config.popup.title=Export Connection Configuration for Starting the Exam sebserver.exam.form.export.config.name=Name diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index 7f5f7cba..464a6fed 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -102,6 +102,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestServiceImpl; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteExamConfigMapping; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteIndicatorTemplate; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ExportExamConfig; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMapping; @@ -2602,7 +2603,9 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { new GetIndicatorTemplatePage(), new NewIndicatorTemplate(), new SaveIndicatorTemplate(), - new GetIndicatorTemplate()); + new DeleteIndicatorTemplate(), + new GetIndicatorTemplate(), + new GetExamConfigNodeNames()); Page examTemplatePage = restService .getBuilder(GetExamTemplatePage.class) @@ -2653,11 +2656,11 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { assertEquals(configTemplate.id, templateFromList.configTemplateId); // create new indicator template - final MultiValueMap thresholds = new LinkedMultiValueMap<>(); + MultiValueMap thresholds = new LinkedMultiValueMap<>(); thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "1|000001"); thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "2|000002"); thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "3|000003"); - final IndicatorTemplate indicatorTemplate = restService + IndicatorTemplate indicatorTemplate = restService .getBuilder(NewIndicatorTemplate.class) .withFormParam(IndicatorTemplate.ATTR_EXAM_TEMPLATE_ID, examTemplate.getModelId()) .withFormParam(Domain.INDICATOR.ATTR_NAME, "Errors") @@ -2683,11 +2686,118 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { assertFalse(indicatorList.isEmpty()); assertTrue(indicatorList.content.size() == 1); - // TODO save exam template + // get exam config template for use + final List configTemplateNames = restService.getBuilder(GetExamConfigNodeNames.class) + .withQueryParam(ConfigurationNode.FILTER_ATTR_TYPE, ConfigurationType.TEMPLATE.name()) + .call() + .getOrThrow(); - // TODO edit indicator template + assertNotNull(configTemplateNames); + assertFalse(configTemplateNames.isEmpty()); + final EntityName configTemplateName = configTemplateNames.get(0); - // TODO remove indicator template + // edit/save exam template + ExamTemplate savedTemplate = restService + .getBuilder(SaveExamTemplate.class) + .withBody(new ExamTemplate( + examTemplate.id, + examTemplate.institutionId, + examTemplate.name, + "New Description", + null, + null, + Long.parseLong(configTemplateName.modelId), // assosiate with given config template + null, + null, + null)) + .call() + .getOrThrow(); + + assertNotNull(savedTemplate); + assertEquals("New Description", savedTemplate.description); + assertNotNull(savedTemplate.configTemplateId); + + // edit/save indicator template + IndicatorTemplate savedIndicatorTemplate = restService + .getBuilder(SaveIndicatorTemplate.class) + .withBody(new IndicatorTemplate( + indicatorTemplate.id, + indicatorTemplate.examTemplateId, + "New Errors", + indicatorTemplate.type, + null, null, null, null)) + .call() + .getOrThrow(); + + assertNotNull(savedIndicatorTemplate); + assertEquals("New Errors", savedIndicatorTemplate.name); + + savedTemplate = restService + .getBuilder(GetExamTemplate.class) + .withURIVariable(API.PARAM_MODEL_ID, savedTemplate.getModelId()) + .call() + .getOrThrow(); + + assertNotNull(savedTemplate); + assertNotNull(savedTemplate.indicatorTemplates); + assertFalse(savedTemplate.indicatorTemplates.isEmpty()); + + savedIndicatorTemplate = savedTemplate.indicatorTemplates.iterator().next(); + assertNotNull(savedIndicatorTemplate); + assertEquals("New Errors", savedIndicatorTemplate.name); + + // create/remove indicator template + thresholds = new LinkedMultiValueMap<>(); + thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "1|000001"); + thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "2|000002"); + thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "3|000003"); + indicatorTemplate = restService + .getBuilder(NewIndicatorTemplate.class) + .withFormParam(IndicatorTemplate.ATTR_EXAM_TEMPLATE_ID, examTemplate.getModelId()) + .withFormParam(Domain.INDICATOR.ATTR_NAME, "Errors") + .withFormParam(Domain.INDICATOR.ATTR_TYPE, IndicatorType.ERROR_COUNT.name) + .withFormParam(Domain.INDICATOR.ATTR_COLOR, "000001") + .withFormParams(thresholds) + .call() + .getOrThrow(); + + savedTemplate = restService + .getBuilder(GetExamTemplate.class) + .withURIVariable(API.PARAM_MODEL_ID, savedTemplate.getModelId()) + .call() + .getOrThrow(); + + assertNotNull(savedTemplate); + assertNotNull(savedTemplate.indicatorTemplates); + assertFalse(savedTemplate.indicatorTemplates.isEmpty()); + assertTrue(savedTemplate.indicatorTemplates.size() == 2); + final Iterator iterator = savedTemplate.indicatorTemplates.iterator(); + final IndicatorTemplate next1 = iterator.next(); + final IndicatorTemplate next2 = iterator.next(); + assertEquals("New Errors", next1.name); + assertEquals("Errors", next2.name); + + final EntityKey entityKey = restService + .getBuilder(DeleteIndicatorTemplate.class) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, savedTemplate.getModelId()) + .withURIVariable(API.PARAM_MODEL_ID, next2.getModelId()) + .call() + .getOrThrow(); + + assertNotNull(entityKey); + savedTemplate = restService + .getBuilder(GetExamTemplate.class) + .withURIVariable(API.PARAM_MODEL_ID, savedTemplate.getModelId()) + .call() + .getOrThrow(); + + assertNotNull(savedTemplate); + assertNotNull(savedTemplate.indicatorTemplates); + assertFalse(savedTemplate.indicatorTemplates.isEmpty()); + assertTrue(savedTemplate.indicatorTemplates.size() == 1); + assertEquals("New Errors", savedTemplate.indicatorTemplates.iterator().next().name); + + // TODO create exam from template // TODO delete exam template