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 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 =
"<?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);
}
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;

View file

@ -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<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.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<Exam> formHandle,

View file

@ -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 =

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.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<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;
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<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)
*
* @param id identifier (primary-key) */

View file

@ -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<Long> 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);
}
}

View file

@ -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();

View file

@ -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<Exam> 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<Exam> 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<Exam> 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<Exam> applyAdditionalSEBRestrictions(Exam exam);
/** Indicates whether a specific exam is been restricted with SEB restriction feature on the LMS or not.

View file

@ -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<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;
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<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
public Result<Exam> 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<Exam> saveAdditionalAttributes(final Exam exam) {
public Result<Exam> saveLMSAttributes(final Exam 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
public Result<Boolean> isRestricted(final Exam exam) {
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.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<Exam, Exam> {
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<Exam, Exam> {
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<Exam, Exam> {
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<Exam, Exam> {
@Override
protected Result<Exam> 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<Exam, Exam> {
return Result.tryCatch(() -> {
this.examSessionService.flushCache(entity);
return entity;
}).flatMap(this.examAdminService::saveAdditionalAttributes);
}).flatMap(this.examAdminService::saveLMSAttributes);
}
@Override

View file

@ -133,15 +133,15 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
EntityType.EXAM_TEMPLATE,
institutionId);
final ExamTemplate examTemplate = super.entityDAO
return super.entityDAO
.byModelId(parentModelId)
.map(t -> 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<ExamTemplate, ExamT
.stream()
.map(i -> {
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;
}

View file

@ -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__

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.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.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.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.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<ExamTemplate> 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<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, "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<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