SEBSERV-162 create exam from template and tests

This commit is contained in:
anhefti 2021-09-15 15:51:02 +02:00
parent a589fd8ad4
commit 25265fdb2b
19 changed files with 721 additions and 128 deletions

View file

@ -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 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 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_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DISPLAY_DATE_FORMAT = "MM-dd-yyyy";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public static final DateTimeFormatter STANDARD_DATE_TIME_FORMATTER = DateTimeFormat public static final DateTimeFormatter STANDARD_DATE_TIME_FORMATTER = DateTimeFormat
.forPattern(DEFAULT_DATE_TIME_FORMAT) .forPattern(DEFAULT_DATE_TIME_FORMAT)
.withZoneUTC(); .withZoneUTC();
public static final DateTimeFormatter STANDARD_DATE_FORMATTER = DateTimeFormat
.forPattern(DEFAULT_DATE_FORMAT)
.withZoneUTC();
public static final String XML_VERSION_HEADER = public static final String XML_VERSION_HEADER =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"; "<?xml version=\"1.0\" encoding=\"utf-8\"?>";

View file

@ -136,6 +136,29 @@ public final class ExamConfigurationMap implements GrantEntity {
this.configStatus = postParams.getEnum(Domain.CONFIGURATION_NODE.ATTR_STATUS, ConfigurationStatus.class); 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 @Override
public EntityType entityType() { public EntityType entityType() {
return EntityType.EXAM_CONFIGURATION_MAP; return EntityType.EXAM_CONFIGURATION_MAP;

View file

@ -712,4 +712,12 @@ public final class Utils {
return false; // Either timeout or unreachable or failed DNS lookup. return false; // Either timeout or unreachable or failed DNS lookup.
} }
} }
public static String replaceAll(final String template, final Map<String, String> values) {
String result = template;
for (final Map.Entry<String, String> e : values.entrySet()) {
result = result.replace(e.getKey(), e.getValue());
}
return result;
}
} }

View file

@ -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.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; 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.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.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
@ -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.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; 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.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle; import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService; 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.CheckExamConsistency;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction; 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.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.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup; 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"); new LocTextKey("sebserver.exam.form.quizurl");
private static final LocTextKey FORM_LMSSETUP_TEXT_KEY = private static final LocTextKey FORM_LMSSETUP_TEXT_KEY =
new LocTextKey("sebserver.exam.form.lmssetup"); 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 = private final static LocTextKey CONSISTENCY_MESSAGE_TITLE =
new LocTextKey("sebserver.exam.consistency.title"); new LocTextKey("sebserver.exam.consistency.title");
@ -329,6 +336,17 @@ public class ExamForm implements TemplateComposer {
.withInputSpan(4) .withInputSpan(4)
.withEmptyCellSeparation(false)) .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( .addField(FormBuilder.singleSelection(
Domain.EXAM.ATTR_TYPE, Domain.EXAM.ATTR_TYPE,
FORM_TYPE_TEXT_KEY, 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( private PageAction importExam(
final PageAction action, final PageAction action,
final FormHandle<Exam> formHandle, final FormHandle<Exam> formHandle,

View file

@ -12,8 +12,6 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -55,8 +53,6 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@GuiProfile @GuiProfile
public class ExamTemplateForm implements TemplateComposer { public class ExamTemplateForm implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(ExamTemplateForm.class);
public static final LocTextKey TITLE_TEXT_KEY = public static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.examtemplate.form.title"); new LocTextKey("sebserver.examtemplate.form.title");
public static final LocTextKey TITLE_NEW_TEXT_KEY = public static final LocTextKey TITLE_NEW_TEXT_KEY =

View file

@ -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.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; 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.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.exam.GetExams;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupNames;
@ -786,4 +787,16 @@ public class ResourceService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public List<Tuple<String>> 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());
}
} }

View file

@ -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<List<EntityName>> {
public GetExamTemplateNames() {
super(new TypeKey<>(
CallType.GET_NAMES,
EntityType.EXAM_TEMPLATE,
new TypeReference<List<EntityName>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_TEMPLATE_ENDPOINT + API.NAMES_PATH_SEGMENT);
}
}

View file

@ -9,6 +9,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao; package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection; 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.api.EntityType;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -55,6 +57,26 @@ public interface AdditionalAttributesDAO {
String name, String name,
String value); 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<Collection<AdditionalAttributeRecord>> saveAdditionalAttributes(
final EntityType type,
final Long entityId,
final Map<String, String> 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) /** Use this to delete an additional attribute by identifier (primary-key)
* *
* @param id identifier (primary-key) */ * @param id identifier (primary-key) */

View file

@ -91,11 +91,17 @@ public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO {
final String value) { final String value) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
if (value == null) { if (value == null) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"value cannot be null. Use delete to delete an additional attribute"); "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<Long> id = this.additionalAttributeRecordMapper final Optional<Long> id = this.additionalAttributeRecordMapper
.selectIdsByExample() .selectIdsByExample()
.where( .where(
@ -150,26 +156,43 @@ public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO {
@Override @Override
@Transactional @Transactional
public void delete(final EntityType type, final Long entityId, final String name) { 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 @Override
@Transactional @Transactional
public void deleteAll(final EntityType type, final Long entityId) { public void deleteAll(final EntityType type, final Long entityId) {
try { try {
if (log.isDebugEnabled()) {
log.debug("Delete all additional attributes. Type: {}, entity: {}",
type, entityId);
}
this.additionalAttributeRecordMapper this.additionalAttributeRecordMapper
.deleteByExample() .deleteByExample()
.where( .where(
@ -181,7 +204,7 @@ public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO {
.build() .build()
.execute(); .execute();
} catch (final Exception e) { } 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);
} }
} }

View file

@ -329,7 +329,8 @@ public class ExamTemplateDAOImpl implements ExamTemplateDAO {
final Long count = this.examTemplateRecordMapper final Long count = this.examTemplateRecordMapper
.countByExample() .countByExample()
.where(ExamTemplateRecordDynamicSqlSupport.name, isEqualTo(examTemplate.name)) .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() .build()
.execute(); .execute();

View file

@ -16,23 +16,17 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringServi
public interface ExamAdminService { 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 * @param exam The Exam to add the LMS specific attributes
* @return Result refer to the Exam with added default indicator or to an error if happened */ * @return Result refer to the created exam or to an error when happened */
Result<Exam> addDefaultIndicator(Exam exam); Result<Exam> saveLMSAttributes(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<Exam> saveAdditionalAttributes(Exam exam);
/** Applies all additional SEB restriction attributes that are defined by the /** Applies all additional SEB restriction attributes that are defined by the
* type of the LMS of a given Exam to this given Exam. * type of the LMS of a given Exam to this given Exam.
* *
* @param exam the Exam to apply all additional SEB restriction attributes * @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<Exam> applyAdditionalSEBRestrictions(Exam exam); Result<Exam> applyAdditionalSEBRestrictions(Exam exam);
/** Indicates whether a specific exam is been restricted with SEB restriction feature on the LMS or not. /** Indicates whether a specific exam is been restricted with SEB restriction feature on the LMS or not.

View file

@ -8,6 +8,41 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam; 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 { 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<Exam> 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<Exam> 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<Exam> initExamConfiguration(Exam exam);
} }

View file

@ -9,7 +9,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -21,19 +20,13 @@ import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; 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.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType; 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.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.OpenEdxSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature; 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.datalayer.batis.model.AdditionalAttributeRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; 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.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.dao.RemoteProctoringRoomDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; 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 static final Logger log = LoggerFactory.getLogger(ExamAdminServiceImpl.class);
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final IndicatorDAO indicatorDAO;
private final AdditionalAttributesDAO additionalAttributesDAO; private final AdditionalAttributesDAO additionalAttributesDAO;
private final LmsAPIService lmsAPIService; private final LmsAPIService lmsAPIService;
private final JSONMapper jsonMapper;
private final Cryptor cryptor; private final Cryptor cryptor;
private final ExamProctoringServiceFactory examProctoringServiceFactory; private final ExamProctoringServiceFactory examProctoringServiceFactory;
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final String defaultIndicatorName;
private final String defaultIndicatorType;
private final String defaultIndicatorColor;
private final String defaultIndicatorThresholds;
protected ExamAdminServiceImpl( protected ExamAdminServiceImpl(
final ExamDAO examDAO, final ExamDAO examDAO,
final IndicatorDAO indicatorDAO,
final AdditionalAttributesDAO additionalAttributesDAO, final AdditionalAttributesDAO additionalAttributesDAO,
final LmsAPIService lmsAPIService, final LmsAPIService lmsAPIService,
final JSONMapper jsonMapper,
final Cryptor cryptor, final Cryptor cryptor,
final ExamProctoringServiceFactory examProctoringServiceFactory, final ExamProctoringServiceFactory examProctoringServiceFactory,
final RemoteProctoringRoomDAO remoteProctoringRoomDAO, 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) {
this.examDAO = examDAO; this.examDAO = examDAO;
this.indicatorDAO = indicatorDAO;
this.additionalAttributesDAO = additionalAttributesDAO; this.additionalAttributesDAO = additionalAttributesDAO;
this.lmsAPIService = lmsAPIService; this.lmsAPIService = lmsAPIService;
this.jsonMapper = jsonMapper;
this.cryptor = cryptor; this.cryptor = cryptor;
this.examProctoringServiceFactory = examProctoringServiceFactory; this.examProctoringServiceFactory = examProctoringServiceFactory;
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.defaultIndicatorName = defaultIndicatorName;
this.defaultIndicatorType = defaultIndicatorType;
this.defaultIndicatorColor = defaultIndicatorColor;
this.defaultIndicatorThresholds = defaultIndicatorThresholds;
}
@Override
public Result<Exam> addDefaultIndicator(final Exam exam) {
return Result.tryCatch(() -> {
final Collection<Indicator.Threshold> thresholds = this.jsonMapper.readValue(
this.defaultIndicatorThresholds,
new TypeReference<Collection<Indicator.Threshold>>() {
});
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 @Override
public Result<Exam> applyAdditionalSEBRestrictions(final Exam exam) { public Result<Exam> applyAdditionalSEBRestrictions(final Exam exam) {
return Result.tryCatch(() -> { 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(); .getOrThrow();
if (lmsSetup.lmsType == LmsType.OPEN_EDX) { if (lmsSetup.lmsType == LmsType.OPEN_EDX) {
@ -161,30 +113,10 @@ public class ExamAdminServiceImpl implements ExamAdminService {
} }
@Override @Override
public Result<Exam> saveAdditionalAttributes(final Exam exam) { public Result<Exam> saveLMSAttributes(final Exam exam) {
return saveAdditionalAttributesForMoodleExams(exam); return saveAdditionalAttributesForMoodleExams(exam);
} }
private Result<Exam> 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 @Override
public Result<Boolean> isRestricted(final Exam exam) { public Result<Boolean> isRestricted(final Exam exam) {
if (exam == null) { if (exam == null) {
@ -386,4 +318,24 @@ public class ExamAdminServiceImpl implements ExamAdminService {
} }
} }
private Result<Exam> 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;
});
}
} }

View file

@ -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<Exam> addDefinedIndicators(final Exam exam) {
if (exam.examTemplateId != null) {
return addIndicatorsFromTemplate(exam);
} else {
return addDefaultIndicator(exam);
}
}
@Override
public Result<Exam> 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<Exam> 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<Exam> 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<Exam> addDefaultIndicator(final Exam exam) {
return Result.tryCatch(() -> {
if (log.isDebugEnabled()) {
log.debug("Init default indicator for exam: {}", exam.externalId);
}
final Collection<Indicator.Threshold> thresholds = this.jsonMapper.readValue(
this.defaultIndicatorThresholds,
new TypeReference<Collection<Indicator.Threshold>>() {
});
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<String, String> 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);
}
}

View file

@ -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.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; 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.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.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService;
@ -86,6 +87,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final UserDAO userDAO; private final UserDAO userDAO;
private final ExamAdminService examAdminService; private final ExamAdminService examAdminService;
private final ExamTemplateService examTemplateService;
private final LmsAPIService lmsAPIService; private final LmsAPIService lmsAPIService;
private final ExamConfigService sebExamConfigService; private final ExamConfigService sebExamConfigService;
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
@ -101,6 +103,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
final LmsAPIService lmsAPIService, final LmsAPIService lmsAPIService,
final UserDAO userDAO, final UserDAO userDAO,
final ExamAdminService examAdminService, final ExamAdminService examAdminService,
final ExamTemplateService examTemplateService,
final ExamConfigService sebExamConfigService, final ExamConfigService sebExamConfigService,
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final SEBRestrictionService sebRestrictionService) { final SEBRestrictionService sebRestrictionService) {
@ -115,6 +118,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
this.examDAO = examDAO; this.examDAO = examDAO;
this.userDAO = userDAO; this.userDAO = userDAO;
this.examAdminService = examAdminService; this.examAdminService = examAdminService;
this.examTemplateService = examTemplateService;
this.lmsAPIService = lmsAPIService; this.lmsAPIService = lmsAPIService;
this.sebExamConfigService = sebExamConfigService; this.sebExamConfigService = sebExamConfigService;
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
@ -459,9 +463,10 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@Override @Override
protected Result<Exam> notifyCreated(final Exam entity) { protected Result<Exam> notifyCreated(final Exam entity) {
return this.examAdminService return this.examTemplateService
.addDefaultIndicator(entity) .addDefinedIndicators(entity)
.flatMap(this.examAdminService::saveAdditionalAttributes) .flatMap(this.examTemplateService::initAdditionalAttributes)
.flatMap(this.examTemplateService::initExamConfiguration)
.flatMap(this.examAdminService::applyAdditionalSEBRestrictions); .flatMap(this.examAdminService::applyAdditionalSEBRestrictions);
} }
@ -470,7 +475,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
this.examSessionService.flushCache(entity); this.examSessionService.flushCache(entity);
return entity; return entity;
}).flatMap(this.examAdminService::saveAdditionalAttributes); }).flatMap(this.examAdminService::saveLMSAttributes);
} }
@Override @Override

View file

@ -133,15 +133,15 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
EntityType.EXAM_TEMPLATE, EntityType.EXAM_TEMPLATE,
institutionId); institutionId);
final ExamTemplate examTemplate = super.entityDAO return super.entityDAO
.byModelId(parentModelId) .byModelId(parentModelId)
.map(t -> t.indicatorTemplates
.stream()
.filter(i -> modelId.equals(i.getModelId()))
.findFirst()
.orElseThrow(() -> new ResourceNotFoundException(EntityType.INDICATOR, parentModelId)))
.getOrThrow(); .getOrThrow();
return examTemplate.indicatorTemplates
.stream()
.filter(i -> modelId.equals(i.getModelId()))
.findFirst()
.orElseThrow(() -> new ResourceNotFoundException(EntityType.INDICATOR, parentModelId));
} }
@RequestMapping( @RequestMapping(
@ -220,7 +220,15 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
.stream() .stream()
.map(i -> { .map(i -> {
if (modelId.equals(i.getModelId())) { 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 { } else {
return i; return i;
} }

View file

@ -71,3 +71,7 @@ sebserver.webservice.proctoring.resetBroadcastOnLeav=true
sebserver.webservice.proctoring.zoom.enableWaitingRoom=false sebserver.webservice.proctoring.zoom.enableWaitingRoom=false
sebserver.webservice.proctoring.zoom.sendRejoinForCollectingRoom=true 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__

View file

@ -485,6 +485,9 @@ sebserver.exam.form.type=Exam Type
sebserver.exam.form.type.tooltip=The type of the exam.<br/><br/>This has only descriptive character for now and can be used to categorise exams within a type sebserver.exam.form.type.tooltip=The type of the exam.<br/><br/>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=Exam Supporter
sebserver.exam.form.supporter.tooltip=A list of users that are allowed to support this exam<br/><br/>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.<br/>A filtered choice will drop down. Select a specific username in the dropdown list to add the user to the list.<br/>To remove a user from the list, just double-click the username on the list. sebserver.exam.form.supporter.tooltip=A list of users that are allowed to support this exam<br/><br/>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.<br/>A filtered choice will drop down. Select a specific username in the dropdown list to add the user to the list.<br/>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.<br/>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.popup.title=Export Connection Configuration for Starting the Exam
sebserver.exam.form.export.config.name=Name sebserver.exam.form.export.config.name=Name

View file

@ -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.RestServiceImpl;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency; 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.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.ExportExamConfig;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMapping; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMapping;
@ -2602,7 +2603,9 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
new GetIndicatorTemplatePage(), new GetIndicatorTemplatePage(),
new NewIndicatorTemplate(), new NewIndicatorTemplate(),
new SaveIndicatorTemplate(), new SaveIndicatorTemplate(),
new GetIndicatorTemplate()); new DeleteIndicatorTemplate(),
new GetIndicatorTemplate(),
new GetExamConfigNodeNames());
Page<ExamTemplate> examTemplatePage = restService Page<ExamTemplate> examTemplatePage = restService
.getBuilder(GetExamTemplatePage.class) .getBuilder(GetExamTemplatePage.class)
@ -2653,11 +2656,11 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
assertEquals(configTemplate.id, templateFromList.configTemplateId); assertEquals(configTemplate.id, templateFromList.configTemplateId);
// create new indicator template // create new indicator template
final MultiValueMap<String, String> thresholds = new LinkedMultiValueMap<>(); MultiValueMap<String, String> thresholds = new LinkedMultiValueMap<>();
thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "1|000001"); thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "1|000001");
thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "2|000002"); thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "2|000002");
thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "3|000003"); thresholds.add(Domain.THRESHOLD.REFERENCE_NAME, "3|000003");
final IndicatorTemplate indicatorTemplate = restService IndicatorTemplate indicatorTemplate = restService
.getBuilder(NewIndicatorTemplate.class) .getBuilder(NewIndicatorTemplate.class)
.withFormParam(IndicatorTemplate.ATTR_EXAM_TEMPLATE_ID, examTemplate.getModelId()) .withFormParam(IndicatorTemplate.ATTR_EXAM_TEMPLATE_ID, examTemplate.getModelId())
.withFormParam(Domain.INDICATOR.ATTR_NAME, "Errors") .withFormParam(Domain.INDICATOR.ATTR_NAME, "Errors")
@ -2683,11 +2686,118 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
assertFalse(indicatorList.isEmpty()); assertFalse(indicatorList.isEmpty());
assertTrue(indicatorList.content.size() == 1); assertTrue(indicatorList.content.size() == 1);
// TODO save exam template // get exam config template for use
final List<EntityName> 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<IndicatorTemplate> 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 // TODO delete exam template