SEBSERV-73 finished SEB Exam config update for running exams

This commit is contained in:
anhefti 2019-10-30 14:14:58 +01:00
parent af6ebf7666
commit 9b9aa4625d
24 changed files with 607 additions and 206 deletions

View file

@ -17,6 +17,7 @@ import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
@ -47,7 +48,8 @@ public final class Exam implements GrantEntity {
null, null,
null, null,
ExamStatus.FINISHED, ExamStatus.FINISHED,
Boolean.FALSE); Boolean.FALSE,
null);
public static final String FILTER_ATTR_TYPE = "type"; public static final String FILTER_ATTR_TYPE = "type";
public static final String FILTER_ATTR_STATUS = "status"; public static final String FILTER_ATTR_STATUS = "status";
@ -115,6 +117,9 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_ACTIVE) @JsonProperty(EXAM.ATTR_ACTIVE)
public final Boolean active; public final Boolean active;
@JsonProperty(EXAM.ATTR_LASTUPDATE)
public final String lastUpdate;
@JsonCreator @JsonCreator
public Exam( public Exam(
@JsonProperty(EXAM.ATTR_ID) final Long id, @JsonProperty(EXAM.ATTR_ID) final Long id,
@ -131,7 +136,8 @@ public final class Exam implements GrantEntity {
@JsonProperty(EXAM.ATTR_OWNER) final String owner, @JsonProperty(EXAM.ATTR_OWNER) final String owner,
@JsonProperty(EXAM.ATTR_SUPPORTER) final Collection<String> supporter, @JsonProperty(EXAM.ATTR_SUPPORTER) final Collection<String> supporter,
@JsonProperty(EXAM.ATTR_STATUS) final ExamStatus status, @JsonProperty(EXAM.ATTR_STATUS) final ExamStatus status,
@JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active) { @JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active,
@JsonProperty(EXAM.ATTR_LASTUPDATE) final String lastUpdate) {
this.id = id; this.id = id;
this.institutionId = institutionId; this.institutionId = institutionId;
@ -145,8 +151,9 @@ public final class Exam implements GrantEntity {
this.type = type; this.type = type;
this.quitPassword = quitPassword; this.quitPassword = quitPassword;
this.owner = owner; this.owner = owner;
this.status = (status != null) ? status : ExamStatus.UP_COMING; this.status = (status != null) ? status : getStatusFromDate(startTime, endTime);
this.active = (active != null) ? active : Boolean.FALSE; this.active = (active != null) ? active : Boolean.FALSE;
this.lastUpdate = lastUpdate;
this.supporter = (supporter != null) this.supporter = (supporter != null)
? Collections.unmodifiableCollection(supporter) ? Collections.unmodifiableCollection(supporter)
@ -167,9 +174,13 @@ public final class Exam implements GrantEntity {
this.type = mapper.getEnum(EXAM.ATTR_TYPE, ExamType.class, ExamType.UNDEFINED); this.type = mapper.getEnum(EXAM.ATTR_TYPE, ExamType.class, ExamType.UNDEFINED);
this.quitPassword = mapper.getString(EXAM.ATTR_QUIT_PASSWORD); this.quitPassword = mapper.getString(EXAM.ATTR_QUIT_PASSWORD);
this.owner = mapper.getString(EXAM.ATTR_OWNER); this.owner = mapper.getString(EXAM.ATTR_OWNER);
this.status = mapper.getEnum(EXAM.ATTR_STATUS, ExamStatus.class); this.status = mapper.getEnum(
EXAM.ATTR_STATUS,
ExamStatus.class,
getStatusFromDate(this.startTime, this.endTime));
this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE); this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE);
this.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER); this.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER);
this.lastUpdate = null;
} }
public Exam(final QuizData quizzData) { public Exam(final QuizData quizzData) {
@ -189,9 +200,10 @@ public final class Exam implements GrantEntity {
this.type = null; this.type = null;
this.quitPassword = null; this.quitPassword = null;
this.owner = null; this.owner = null;
this.status = (status != null) ? status : ExamStatus.UP_COMING; this.status = (status != null) ? status : getStatusFromDate(this.startTime, this.endTime);
this.active = null; this.active = null;
this.supporter = null; this.supporter = null;
this.lastUpdate = null;
} }
@Override @Override
@ -326,4 +338,17 @@ public final class Exam implements GrantEntity {
return builder.toString(); return builder.toString();
} }
public static ExamStatus getStatusFromDate(final DateTime startTime, final DateTime endTime) {
final DateTime now = DateTime.now(DateTimeZone.UTC);
if (startTime != null && now.isBefore(startTime)) {
return ExamStatus.UP_COMING;
} else if (startTime != null && now.isAfter(startTime) && (endTime == null || now.isBefore(endTime))) {
return ExamStatus.RUNNING;
} else if (endTime != null && now.isAfter(endTime)) {
return ExamStatus.FINISHED;
} else {
return ExamStatus.UP_COMING;
}
}
} }

View file

@ -23,6 +23,8 @@ import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessageError;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
@ -37,6 +39,7 @@ import ch.ethz.seb.sebserver.gui.service.examconfig.impl.AttributeMapping;
import ch.ethz.seb.sebserver.gui.service.examconfig.impl.ViewContext; import ch.ethz.seb.sebserver.gui.service.examconfig.impl.ViewContext;
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.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
@ -61,10 +64,12 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
"sebserver.examconfig.action.saveToHistory.success"; "sebserver.examconfig.action.saveToHistory.success";
private static final String KEY_UNDO_SUCCESS = private static final String KEY_UNDO_SUCCESS =
"sebserver.examconfig.action.undo.success"; "sebserver.examconfig.action.undo.success";
private static final LocTextKey TITLE_TEXT_KEY = private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.examconfig.props.from.title"); new LocTextKey("sebserver.examconfig.props.from.title");
private static final LocTextKey MESSAGE_SAVE_INTEGRITY_VIOLATION =
new LocTextKey("sebserver.examconfig.action.saveToHistory.integrity-violation");
private final PageService pageService; private final PageService pageService;
private final RestService restService; private final RestService restService;
private final CurrentUser currentUser; private final CurrentUser currentUser;
@ -150,8 +155,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
this.restService.getBuilder(SaveExamConfigHistory.class) this.restService.getBuilder(SaveExamConfigHistory.class)
.withURIVariable(API.PARAM_MODEL_ID, configuration.getModelId()) .withURIVariable(API.PARAM_MODEL_ID, configuration.getModelId())
.call() .call()
.onError(pageContext::notifyError) .onError(t -> notifyErrorOnSave(t, pageContext));
.getOrThrow();
return action; return action;
}) })
.withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS) .withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS)
@ -164,7 +168,6 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
this.restService.getBuilder(SebExamConfigUndo.class) this.restService.getBuilder(SebExamConfigUndo.class)
.withURIVariable(API.PARAM_MODEL_ID, configuration.getModelId()) .withURIVariable(API.PARAM_MODEL_ID, configuration.getModelId())
.call() .call()
.onError(pageContext::notifyError)
.getOrThrow(); .getOrThrow();
return action; return action;
}) })
@ -175,9 +178,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP) .newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.ignoreMoveAwayFromEdit() .ignoreMoveAwayFromEdit()
.publish() .publish();
;
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
log.error("Unexpected error while trying to fetch exam configuration data and create views", e); log.error("Unexpected error while trying to fetch exam configuration data and create views", e);
@ -186,7 +187,24 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
log.error("Unexpected error while trying to fetch exam configuration data and create views", e); log.error("Unexpected error while trying to fetch exam configuration data and create views", e);
pageContext.notifyError(e); pageContext.notifyError(e);
} }
}
private void notifyErrorOnSave(final Throwable error, final PageContext context) {
if (error instanceof APIMessageError) {
try {
final List<APIMessage> errorMessages = ((APIMessageError) error).getErrorMessages();
final APIMessage apiMessage = errorMessages.get(0);
if (APIMessage.ErrorMessage.INTEGRITY_VALIDATION.isOf(apiMessage)) {
throw new PageMessageException(MESSAGE_SAVE_INTEGRITY_VIOLATION);
} else {
throw error;
}
} catch (final PageMessageException e) {
throw e;
} catch (final Throwable e) {
throw new RuntimeException(error);
}
}
} }
} }

View file

@ -303,7 +303,6 @@ public class PageContextImpl implements PageContext {
@Override @Override
public <T> T notifyError(final Throwable error) { public <T> T notifyError(final Throwable error) {
log.error("Unexpected error: ", error);
notifyError(error.getMessage(), error); notifyError(error.getMessage(), error);
return null; return null;
} }

View file

@ -38,7 +38,7 @@ public interface ExamConfigurationMapDAO extends
* @param examId The Exam identifier * @param examId The Exam identifier
* @return ConfigurationNode identifier of the default Exam Configuration of * @return ConfigurationNode identifier of the default Exam Configuration of
* the Exam with specified identifier */ * the Exam with specified identifier */
Result<Long> getDefaultConfigurationForExam(Long examId); Result<Long> getDefaultConfigurationNode(Long examId);
/** Get the ConfigurationNode identifier of the Exam Configuration of /** Get the ConfigurationNode identifier of the Exam Configuration of
* the Exam for a specified user identifier. * the Exam for a specified user identifier.
@ -47,7 +47,7 @@ public interface ExamConfigurationMapDAO extends
* @param userId the user identifier * @param userId the user identifier
* @return ConfigurationNode identifier of the Exam Configuration of * @return ConfigurationNode identifier of the Exam Configuration of
* the Exam for a specified user identifier */ * the Exam for a specified user identifier */
Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId); Result<Long> getUserConfigurationNodeId(final Long examId, final String userId);
/** Get all id of Exams that has a relation to the given configuration id. /** Get all id of Exams that has a relation to the given configuration id.
* *

View file

@ -44,11 +44,13 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
Result<Collection<Exam>> allForEndCheck(); Result<Collection<Exam>> allForEndCheck();
Result<Exam> startUpdate(Long examId, String update); Result<Exam> placeLock(Long examId, String update);
Result<Exam> endUpdate(Long examId, String update); Result<Exam> releaseLock(Long examId, String update);
Result<Boolean> isUpdating(Long examId); Result<Long> forceUnlock(Long examId);
Result<Boolean> isLocked(Long examId);
Result<Boolean> upToDate(Long examId, String lastUpdate); Result<Boolean> upToDate(Long examId, String lastUpdate);

View file

@ -21,6 +21,7 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
@ -148,7 +149,7 @@ public class ConfigurationDAOImpl implements ConfigurationDAO {
.execute(); .execute();
Collections.sort( Collections.sort(
configs, configs,
(c1, c2) -> c1.getVersionDate().compareTo(c2.getVersionDate())); (c1, c2) -> c1.getVersionDate().compareTo(c2.getVersionDate()) * -1);
final ConfigurationRecord configurationRecord = configs.get(0); final ConfigurationRecord configurationRecord = configs.get(0);
return configurationRecord; return configurationRecord;
}).flatMap(ConfigurationDAOImpl::toDomainModel); }).flatMap(ConfigurationDAOImpl::toDomainModel);
@ -177,7 +178,7 @@ public class ConfigurationDAOImpl implements ConfigurationDAO {
} }
@Override @Override
@Transactional @Transactional(propagation = Propagation.REQUIRES_NEW)
public Result<Configuration> saveToHistory(final Long configurationNodeId) { public Result<Configuration> saveToHistory(final Long configurationNodeId) {
return this.configurationDAOBatchService return this.configurationDAOBatchService
.saveToHistory(configurationNodeId) .saveToHistory(configurationNodeId)

View file

@ -173,7 +173,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Long> getDefaultConfigurationForExam(final Long examId) { public Result<Long> getDefaultConfigurationNode(final Long examId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
.selectByExample() .selectByExample()
.where( .where(
@ -190,7 +190,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
} }
@Override @Override
public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { public Result<Long> getUserConfigurationNodeId(final Long examId, final String userId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
.selectByExample() .selectByExample()
.where( .where(

View file

@ -27,6 +27,7 @@ import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
@ -172,13 +173,14 @@ public class ExamDAOImpl implements ExamDAO {
(exam.supporter != null) (exam.supporter != null)
? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR) ? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR)
: null, : null,
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(), (exam.type != null) ? exam.type.name() : null,
exam.quitPassword, exam.quitPassword,
null, // browser keys null, // browser keys
null, // status (exam.status != null) ? exam.status.name() : null,
null, // updating null, // updating
null, // lastUpdate null, // lastUpdate
BooleanUtils.toIntegerObject(exam.active)); null // active
);
this.examRecordMapper.updateByPrimaryKeySelective(examRecord); this.examRecordMapper.updateByPrimaryKeySelective(examRecord);
return this.examRecordMapper.selectByPrimaryKey(exam.id); return this.examRecordMapper.selectByPrimaryKey(exam.id);
@ -230,8 +232,8 @@ public class ExamDAOImpl implements ExamDAO {
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(), (exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
null, // quitPassword null, // quitPassword
null, // browser keys null, // browser keys
null, // status (exam.status != null) ? exam.status.name() : ExamStatus.UP_COMING.name(),
null, // updating BooleanUtils.toInteger(false),
null, // lastUpdate null, // lastUpdate
BooleanUtils.toInteger(true)); BooleanUtils.toInteger(true));
@ -329,8 +331,8 @@ public class ExamDAOImpl implements ExamDAO {
} }
@Override @Override
@Transactional @Transactional(propagation = Propagation.REQUIRES_NEW)
public Result<Exam> startUpdate(final Long examId, final String update) { public Result<Exam> placeLock(final Long examId, final String update) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final ExamRecord examRec = this.recordById(examId) final ExamRecord examRec = this.recordById(examId)
@ -357,8 +359,8 @@ public class ExamDAOImpl implements ExamDAO {
} }
@Override @Override
@Transactional @Transactional(propagation = Propagation.REQUIRES_NEW)
public Result<Exam> endUpdate(final Long examId, final String update) { public Result<Exam> releaseLock(final Long examId, final String update) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final ExamRecord examRec = this.recordById(examId) final ExamRecord examRec = this.recordById(examId)
@ -386,9 +388,29 @@ public class ExamDAOImpl implements ExamDAO {
.onError(TransactionHandler::rollback); .onError(TransactionHandler::rollback);
} }
@Override
@Transactional
public Result<Long> forceUnlock(final Long examId) {
log.info("forceUnlock for exam: {}", examId);
return Result.tryCatch(() -> {
final ExamRecord examRecord = new ExamRecord(
examId,
null, null, null, null, null, null, null, null, null,
BooleanUtils.toInteger(false),
null, null);
this.examRecordMapper.updateByPrimaryKeySelective(examRecord);
return examRecord.getId();
})
.onError(TransactionHandler::rollback);
}
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Boolean> isUpdating(final Long examId) { public Result<Boolean> isLocked(final Long examId) {
return this.recordById(examId) return this.recordById(examId)
.map(rec -> BooleanUtils.toBooleanObject(rec.getUpdating())); .map(rec -> BooleanUtils.toBooleanObject(rec.getUpdating()));
} }
@ -397,7 +419,13 @@ public class ExamDAOImpl implements ExamDAO {
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Boolean> upToDate(final Long examId, final String lastUpdate) { public Result<Boolean> upToDate(final Long examId, final String lastUpdate) {
return this.recordById(examId) return this.recordById(examId)
.map(rec -> lastUpdate.equals(rec.getLastupdate())); .map(rec -> {
if (lastUpdate == null) {
return rec.getLastupdate() == null;
} else {
return lastUpdate.equals(rec.getLastupdate());
}
});
} }
@Override @Override
@ -588,7 +616,8 @@ public class ExamDAOImpl implements ExamDAO {
record.getOwner(), record.getOwner(),
supporter, supporter,
status, status,
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : 0)); BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : 0),
record.getLastupdate());
}); });
} }

View file

@ -15,9 +15,12 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
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;
@ -53,26 +56,35 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
final LmsType lmsType = lmsSetup.getLmsType(); final LmsType lmsType = lmsSetup.getLmsType();
this.mockups = new ArrayList<>(); this.mockups = new ArrayList<>();
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1", "Demo Quit Mockup", "quiz1", institutionId, lmsSetupId, lmsType, "Demo Quiz 1", "Demo Quiz Mockup",
"2020-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); "2020-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2", "Demo Quit Mockup", "quiz2", institutionId, lmsSetupId, lmsType, "Demo Quiz 2", "Demo Quiz Mockup",
"2020-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); "2020-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3", "Demo Quit Mockup", "quiz3", institutionId, lmsSetupId, lmsType, "Demo Quiz 3", "Demo Quiz Mockup",
"2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/")); "2018-07-30T09:00:00Z", "2018-08-01T00:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4", "Demo Quit Mockup", "quiz4", institutionId, lmsSetupId, lmsType, "Demo Quiz 4", "Demo Quiz Mockup",
"2018-01-01T00:00:00Z", "2019-01-01T00:00:00Z", "http://lms.mockup.com/api/")); "2018-01-01T00:00:00Z", "2019-01-01T00:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5", "Demo Quit Mockup", "quiz5", institutionId, lmsSetupId, lmsType, "Demo Quiz 5", "Demo Quiz Mockup",
"2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); "2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6", "Demo Quit Mockup", "quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6", "Demo Quiz Mockup",
"2019-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); "2019-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7", "Demo Quit Mockup", "quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7", "Demo Quiz Mockup",
"2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); "2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData(
"quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10",
"Starts in a minute and ends after five minutes",
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
"http://lms.mockup.com/api/"));
} }
@Override @Override

View file

@ -1,19 +0,0 @@
/*
* Copyright (c) 2019 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.sebconfig;
public class ConfigurationChangedEvent {
public final Long configurationId;
public ConfigurationChangedEvent(final Long configurationId) {
this.configurationId = configurationId;
}
}

View file

@ -147,11 +147,11 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
} }
public Result<Long> getDefaultConfigurationIdForExam(final Long examId) { public Result<Long> getDefaultConfigurationIdForExam(final Long examId) {
return this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId); return this.examConfigurationMapDAO.getDefaultConfigurationNode(examId);
} }
public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) { public Result<Long> getUserConfigurationIdForExam(final Long examId, final String userId) {
return this.examConfigurationMapDAO.getUserConfigurationIdForExam(examId, userId); return this.examConfigurationMapDAO.getUserConfigurationNodeId(examId, userId);
} }
@Override @Override

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2019 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.session;
import java.util.Collection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ExamConfigUpdateService {
Logger log = LoggerFactory.getLogger(ExamConfigUpdateService.class);
Result<Collection<Long>> processSEBExamConfigurationChange(Long configurationNodeId);
@Transactional
default Result<Configuration> processSEBExamConfigurationChange(final Configuration config) {
if (config == null) {
return Result.ofError(new NullPointerException("Configuration has null reference"));
}
return Result.tryCatch(() -> {
processSEBExamConfigurationChange(config.configurationNodeId)
.map(ids -> {
log.info("Successfully updated SEB Configuration for exams: {}", ids);
return ids;
})
.getOrThrow();
return config;
});
}
void forceReleaseUpdateLocks(Long configurationId);
Collection<Result<Long>> forceReleaseUpdateLocks(Collection<Long> examIds);
Result<Collection<Long>> checkRunningExamIntegrity(final Long configurationNodeId);
}

View file

@ -12,15 +12,12 @@ import java.io.OutputStream;
import java.util.Collection; import java.util.Collection;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.context.event.EventListener;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
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.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
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.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent;
/** A Service to handle running exam sessions */ /** A Service to handle running exam sessions */
public interface ExamSessionService { public interface ExamSessionService {
@ -47,6 +44,12 @@ public interface ExamSessionService {
* @return true if an Exam is currently running */ * @return true if an Exam is currently running */
boolean isExamRunning(Long examId); boolean isExamRunning(Long examId);
/** Indicates if the Exam with specified Id is currently locked for new SEB Client connection attempts.
*
* @param examId The Exam identifier
* @return true if the specified Exam is currently locked for new SEB Client connections. */
boolean isExamLocked(Long examId);
/** Use this to get currently running exams by exam identifier. /** Use this to get currently running exams by exam identifier.
* This test first if the Exam with the given identifier is currently/still * This test first if the Exam with the given identifier is currently/still
* running. If true the Exam is returned within the result. Otherwise the * running. If true the Exam is returned within the result. Otherwise the
@ -99,7 +102,17 @@ public interface ExamSessionService {
* of a running exam */ * of a running exam */
Result<Collection<ClientConnectionData>> getConnectionData(Long examId); Result<Collection<ClientConnectionData>> getConnectionData(Long examId);
@EventListener(ConfigurationChangedEvent.class) /** Use this to check if the current cached running exam is up to date
void updateExamConfigCache(ConfigurationChangedEvent configChanged); * and if not to flush the cache.
*
* @param examId the Exam identifier
* @return Result with updated Exam instance or refer to an error if happened */
Result<Exam> updateExamCache(Long examId);
/** Flush all the caches for an specified Exam.
*
* @param exam The Exam instance
* @return Result with reference to the given Exam or to an error if happened */
Result<Exam> flushCache(final Exam exam);
} }

View file

@ -0,0 +1,302 @@
/*
* Copyright (c) 2019 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.session.impl;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
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.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@Lazy
@Service
@WebServiceProfile
public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
private static final Logger log = LoggerFactory.getLogger(ExamConfigUpdateServiceImpl.class);
private final ExamDAO examDAO;
private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ExamSessionService examSessionService;
private final ExamSessionControlTask examSessionControlTask;
protected ExamConfigUpdateServiceImpl(
final ExamDAO examDAO,
final ConfigurationDAO configurationDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ExamSessionService examSessionService,
final ExamSessionControlTask examSessionControlTask) {
this.examDAO = examDAO;
this.configurationDAO = configurationDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.examSessionService = examSessionService;
this.examSessionControlTask = examSessionControlTask;
}
// processing:
// check running exam integrity (No running exam with active SEB client-connection available)
// if OK, create an update-id and for each exam, create an update-lock on DB (This also prevents new SEB client connection attempts during update)
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
// store the new configuration values (into history) so that they take effect
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
// evict each Exam from cache and release the update-lock on DB
@Override
@Transactional
public Result<Collection<Long>> processSEBExamConfigurationChange(final Long configurationNodeId) {
final String updateId = this.examSessionControlTask.createUpdateId();
if (log.isDebugEnabled()) {
log.debug("Process SEB Exam Configuration update for: {} with update-id {}",
configurationNodeId,
updateId);
}
return Result.tryCatch(() -> {
// check running exam integrity (No running exam with active SEB client-connection available)
final Collection<Long> examIdsFirstCheck = checkRunningExamIntegrity(configurationNodeId)
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("Involved exams on fist integrity check: {}", examIdsFirstCheck);
}
// if OK, create an update-lock on DB (This also prevents new SEB client connection attempts during update)
final Collection<Exam> exams = lockForUpdate(examIdsFirstCheck, updateId)
.stream()
.map(Result::getOrThrow)
.collect(Collectors.toList());
final Collection<Long> examsIds = exams
.stream()
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Update-Lock successfully placed for all involved exams: {}", examsIds);
}
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
final Collection<Long> examIdsSecondCheck = checkRunningExamIntegrity(configurationNodeId)
.getOrThrow();
checkIntegrityDoubleCheck(
examIdsFirstCheck,
examIdsSecondCheck);
if (log.isDebugEnabled()) {
log.debug("Involved exams on second integrity check: {}", examIdsSecondCheck);
}
// store the new configuration values (into history) so that they take effect
final Configuration configuration = this.configurationDAO
.saveToHistory(configurationNodeId)
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("Successfully save SEB Exam Configuration: {}", configuration);
}
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
final Collection<Long> updatedExams = updateConfigKey(exams)
.stream()
.map(Result::getOrThrow)
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Successfully updated ConfigKey for Exams: {}", updatedExams);
}
// evict each Exam from cache and release the update-lock on DB
final Collection<Long> evictedExams = evictFromCache(exams)
.stream()
.filter(Result::hasValue)
.map(Result::get)
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Successfully evicted Exams from cache: {}", evictedExams);
}
// release the update-locks on involved exams
final Collection<Long> releasedLocks = releaseUpdateLocks(examIdsFirstCheck, updateId)
.stream()
.map(Result::getOrThrow)
.map(Exam::getId)
.collect(Collectors.toList());
if (log.isDebugEnabled()) {
log.debug("Successfully released update-locks on Exams: {}", releasedLocks);
}
return examIdsFirstCheck;
})
.onError(TransactionHandler::rollback);
}
@Override
public void forceReleaseUpdateLocks(final Long configurationId) {
log.warn(" **** Force release of update-locks for all exams that are related to configuration: {}",
configurationId);
try {
final Configuration config = this.configurationDAO.byPK(configurationId)
.getOrThrow();
final Collection<Long> involvedExams = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(config.configurationNodeId)
.getOrThrow();
final Collection<Long> examsIds = forceReleaseUpdateLocks(involvedExams)
.stream()
.map(Result::getOrThrow)
.collect(Collectors.toList());
log.info("Successfully released update-locks for exams: {}", examsIds);
} catch (final Exception e) {
log.error("Failed to release update-locks for exam(s)", e);
}
}
@Override
public Collection<Result<Long>> forceReleaseUpdateLocks(final Collection<Long> examIds) {
return examIds.stream()
.map(this.examDAO::forceUnlock)
.collect(Collectors.toList());
}
private void checkIntegrityDoubleCheck(
final Collection<Long> examIdsFirstCheck,
final Collection<Long> examIdsSecondCheck) {
if (examIdsFirstCheck.size() != examIdsSecondCheck.size()) {
throw new IllegalStateException("Running Exam integrity check missmatch. examIdsFirstCheck: "
+ examIdsFirstCheck + " examIdsSecondCheck: " + examIdsSecondCheck);
}
}
private Collection<Result<Exam>> lockForUpdate(final Collection<Long> examIds, final String update) {
return examIds.stream()
.map(id -> this.examDAO.placeLock(id, update))
.collect(Collectors.toList());
}
private Collection<Result<Exam>> releaseUpdateLocks(final Collection<Long> examIds, final String update) {
return examIds.stream()
.map(id -> this.examDAO.releaseLock(id, update))
.collect(Collectors.toList());
}
private Collection<Result<Exam>> updateConfigKey(final Collection<Exam> exams) {
return exams
.stream()
.map(this::updateConfigKey)
.collect(Collectors.toList());
}
private Result<Exam> updateConfigKey(final Exam exam) {
return Result.tryCatch(() -> {
if (log.isDebugEnabled()) {
log.debug("update Config Key for Exam {}", exam.externalId);
}
// TODO
return exam;
});
}
private Collection<Result<Exam>> evictFromCache(final Collection<Exam> exams) {
return exams
.stream()
.map(this.examSessionService::flushCache)
.collect(Collectors.toList());
}
@Override
public Result<Collection<Long>> checkRunningExamIntegrity(final Long configurationNodeId) {
final Collection<Long> involvedExams = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(configurationNodeId)
.getOrThrow();
if (involvedExams == null || involvedExams.isEmpty()) {
return Result.of(Collections.emptyList());
}
// check if the configuration is attached to a running exams with active client connections
final long activeConnections = involvedExams
.stream()
.flatMap(examId -> {
return this.examSessionService.getConnectionData(examId)
.getOrThrow()
.stream();
})
.filter(ExamConfigUpdateServiceImpl::isActiveConnection)
.count();
// if we have active SEB client connection on any running exam that
// is involved within the specified configuration change, the change is denied
if (activeConnections > 0) {
return Result.ofError(new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,
"Integrity violation: There are currently active SEB Client connection."));
} else {
// otherwise we return the involved identifiers exams to further processing
return Result.of(involvedExams);
}
}
private static boolean isActiveConnection(final ClientConnectionData connection) {
if (connection.clientConnection.status == ConnectionStatus.ESTABLISHED
|| connection.clientConnection.status == ConnectionStatus.AUTHENTICATED) {
return true;
}
if (connection.clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
final Long creationTime = connection.clientConnection.getCreationTime();
final long millisecondsNow = Utils.getMillisecondsNow();
if (millisecondsNow - creationTime < 30 * Constants.SECOND_IN_MILLIS) {
return true;
}
}
return false;
}
}

View file

@ -100,8 +100,7 @@ public class ExamSessionCacheService {
@CacheEvict( @CacheEvict(
cacheNames = CACHE_NAME_RUNNING_EXAM, cacheNames = CACHE_NAME_RUNNING_EXAM,
key = "#exam.id", key = "#exam.id")
condition = "#target.isRunning(#result)")
public Exam evict(final Exam exam) { public Exam evict(final Exam exam) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
@ -111,7 +110,7 @@ public class ExamSessionCacheService {
return exam; return exam;
} }
boolean isRunning(final Exam exam) { public boolean isRunning(final Exam exam) {
if (exam == null) { if (exam == null) {
return false; return false;
} }
@ -131,7 +130,6 @@ public class ExamSessionCacheService {
default: { default: {
return false; return false;
} }
} }
} }
@ -166,33 +164,31 @@ public class ExamSessionCacheService {
@Cacheable( @Cacheable(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId", key = "#exam.id",
unless = "#result == null") unless = "#result == null")
public InMemorySebConfig getDefaultSebConfigForExam(final Long examId) { public InMemorySebConfig getDefaultSebConfigForExam(final Exam exam) {
final Exam runningExam = this.getRunningExam(examId);
try { try {
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
final Long configId = this.sebExamConfigService.exportForExam( final Long configId = this.sebExamConfigService.exportForExam(
byteOut, byteOut,
runningExam.institutionId, exam.institutionId,
examId); exam.id);
return new InMemorySebConfig(configId, runningExam.id, byteOut.toByteArray()); return new InMemorySebConfig(configId, exam.id, byteOut.toByteArray());
} catch (final Exception e) { } catch (final Exception e) {
log.error("Unexpected error while getting default exam configuration for running exam; {}", runningExam, e); log.error("Unexpected error while getting default exam configuration for running exam; {}", exam, e);
return null; return null;
} }
} }
@CacheEvict( @CacheEvict(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM, cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId") key = "#exam.id")
public void evictDefaultSebConfig(final Long examId) { public void evictDefaultSebConfig(final Exam exam) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Eviction of default SEB Configuration from cache for exam: {}", examId); log.debug("Eviction of default SEB Configuration from cache for exam: {}", exam.id);
} }
} }

View file

@ -29,7 +29,7 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
@Service @Service
public class ExamSessionControlTask { class ExamSessionControlTask {
private static final Logger log = LoggerFactory.getLogger(ExamSessionControlTask.class); private static final Logger log = LoggerFactory.getLogger(ExamSessionControlTask.class);
@ -47,20 +47,19 @@ public class ExamSessionControlTask {
this.examDAO = examDAO; this.examDAO = examDAO;
this.examTimePrefix = examTimePrefix; this.examTimePrefix = examTimePrefix;
this.examTimeSuffix = examTimeSuffix; this.examTimeSuffix = examTimeSuffix;
this.updatePrefix = webserviceInfo.getHostAddress() this.updatePrefix = webserviceInfo.getHostAddress()
+ "_" + webserviceInfo.getServerPort() + "_"; + "_" + webserviceInfo.getServerPort() + "_";
} }
@Async @Async
@Scheduled(cron = "1 * * * * *") @Scheduled(cron = "${sebserver.webservice.api.exam.update-interval:1 * * * * *}")
@Transactional @Transactional
public void execTask() { public void execTask() {
final String updateId = createUpdateId(); final String updateId = this.createUpdateId();
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Run ExamControlTask. Update Id: {}", updateId); log.debug("Run exam runtime update task with Id: {}", updateId);
} }
controlStart(updateId); controlStart(updateId);
@ -84,7 +83,9 @@ public class ExamSessionControlTask {
.map(exam -> this.setRunning(exam, updateId)) .map(exam -> this.setRunning(exam, updateId))
.collect(Collectors.toMap(Exam::getId, Exam::getName)); .collect(Collectors.toMap(Exam::getId, Exam::getName));
log.info("Updated exams to running state: {}", updated); if (!updated.isEmpty()) {
log.info("Updated exams to running state: {}", updated);
}
} catch (final Exception e) { } catch (final Exception e) {
log.error("Unexpected error while trying to update exams: ", e); log.error("Unexpected error while trying to update exams: ", e);
@ -97,15 +98,15 @@ public class ExamSessionControlTask {
final DateTime now = DateTime.now(DateTimeZone.UTC); final DateTime now = DateTime.now(DateTimeZone.UTC);
if (exam.getStatus() == ExamStatus.UP_COMING if (exam.getStatus() == ExamStatus.UP_COMING
&& exam.endTime.plus(this.examTimeSuffix).isBefore(now)) { && exam.endTime.plus(this.examTimeSuffix).isBefore(now)) {
return setRunning(exam, createUpdateId()); return setRunning(exam, this.createUpdateId());
} else { } else {
return exam; return exam;
} }
}); });
} }
public Result<Exam> setRunning(final Exam exam) { public String createUpdateId() {
return Result.tryCatch(() -> setRunning(exam, createUpdateId())); return this.updatePrefix + Utils.getMillisecondsNow();
} }
private Exam setRunning(final Exam exam, final String updateId) { private Exam setRunning(final Exam exam, final String updateId) {
@ -114,11 +115,11 @@ public class ExamSessionControlTask {
} }
return this.examDAO return this.examDAO
.startUpdate(exam.id, updateId) .placeLock(exam.id, updateId)
.flatMap(e -> this.examDAO.save(new Exam( .flatMap(e -> this.examDAO.save(new Exam(
exam.id, exam.id,
ExamStatus.RUNNING))) ExamStatus.RUNNING)))
.flatMap(e -> this.examDAO.endUpdate(e.id, updateId)) .flatMap(e -> this.examDAO.releaseLock(e.id, updateId))
.getOrThrow(); .getOrThrow();
} }
@ -138,7 +139,9 @@ public class ExamSessionControlTask {
.map(exam -> this.setFinished(exam, updateId)) .map(exam -> this.setFinished(exam, updateId))
.collect(Collectors.toMap(Exam::getId, Exam::getName)); .collect(Collectors.toMap(Exam::getId, Exam::getName));
log.info("Updated exams to finished state: {}", updated); if (!updated.isEmpty()) {
log.info("Updated exams to finished state: {}", updated);
}
} catch (final Exception e) { } catch (final Exception e) {
log.error("Unexpected error while trying to update exams: ", e); log.error("Unexpected error while trying to update exams: ", e);
@ -151,16 +154,12 @@ public class ExamSessionControlTask {
} }
return this.examDAO return this.examDAO
.startUpdate(exam.id, updateId) .placeLock(exam.id, updateId)
.flatMap(e -> this.examDAO.save(new Exam( .flatMap(e -> this.examDAO.save(new Exam(
exam.id, exam.id,
ExamStatus.FINISHED))) ExamStatus.FINISHED)))
.flatMap(e -> this.examDAO.endUpdate(e.id, updateId)) .flatMap(e -> this.examDAO.releaseLock(e.id, updateId))
.getOrThrow(); .getOrThrow();
} }
private String createUpdateId() {
return this.updatePrefix + Utils.getMillisecondsNow();
}
} }

View file

@ -17,12 +17,12 @@ import java.util.NoSuchElementException;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -39,7 +39,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@Lazy @Lazy
@ -92,7 +91,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
} }
// check SEB configuration // check SEB configuration
this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId) this.examConfigurationMapDAO.getDefaultConfigurationNode(examId)
.get(t -> { .get(t -> {
result.add(ErrorMessage.EXAM_CONSISTANCY_VALIDATION_CONFIG.of(exam.getModelId())); result.add(ErrorMessage.EXAM_CONSISTANCY_VALIDATION_CONFIG.of(exam.getModelId()));
return null; return null;
@ -116,6 +115,17 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return !getRunningExam(examId).hasError(); return !getRunningExam(examId).hasError();
} }
@Override
public boolean isExamLocked(final Long examId) {
final Result<Boolean> locked = this.examDAO.isLocked(examId);
if (locked.hasError()) {
log.error("Unexpected Error while trying to verify lock for Exam: {}", examId);
}
return locked.hasError() || BooleanUtils.toBoolean(locked.get());
}
@Override @Override
public Result<Exam> getRunningExam(final Long examId) { public Result<Exam> getRunningExam(final Long examId) {
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
@ -123,6 +133,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
} }
final Exam exam = this.examSessionCacheService.getRunningExam(examId); final Exam exam = this.examSessionCacheService.getRunningExam(examId);
if (this.examSessionCacheService.isRunning(exam)) { if (this.examSessionCacheService.isRunning(exam)) {
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
log.trace("Exam {} is running and cached", examId); log.trace("Exam {} is running and cached", examId);
@ -194,8 +205,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
log.debug("Trying to get exam from InMemorySebConfig"); log.debug("Trying to get exam from InMemorySebConfig");
} }
final Exam exam = this.getRunningExam(connection.examId)
.getOrThrow();
final InMemorySebConfig sebConfigForExam = this.examSessionCacheService final InMemorySebConfig sebConfigForExam = this.examSessionCacheService
.getDefaultSebConfigForExam(connection.examId); .getDefaultSebConfigForExam(exam);
if (sebConfigForExam == null) { if (sebConfigForExam == null) {
log.error("Failed to get and cache InMemorySebConfig for connection: {}", connection); log.error("Failed to get and cache InMemorySebConfig for connection: {}", connection);
@ -241,23 +255,24 @@ public class ExamSessionServiceImpl implements ExamSessionService {
} }
@Override @Override
@EventListener(ConfigurationChangedEvent.class) public Result<Exam> updateExamCache(final Long examId) {
public void updateExamConfigCache(final ConfigurationChangedEvent configChanged) { final Exam exam = this.examSessionCacheService.getRunningExam(examId);
final Boolean isUpToDate = this.examDAO.upToDate(examId, exam.lastUpdate)
.onError(t -> log.error("Failed to verify if cached exam is up to date: {}", exam, t))
.getOr(false);
if (log.isDebugEnabled()) { if (!BooleanUtils.toBoolean(isUpToDate)) {
log.debug("Flush exam config cache for configuration: {}", configChanged.configurationId); return flushCache(exam);
} else {
return Result.of(exam);
} }
this.examConfigurationMapDAO
.getExamIdsForConfigId(configChanged.configurationId)
.getOrElse(() -> Collections.emptyList())
.forEach(this.examSessionCacheService::evictDefaultSebConfig);
} }
private void flushCache(final Exam exam) { @Override
try { public Result<Exam> flushCache(final Exam exam) {
return Result.tryCatch(() -> {
this.examSessionCacheService.evict(exam); this.examSessionCacheService.evict(exam);
this.examSessionCacheService.evictDefaultSebConfig(exam.id); this.examSessionCacheService.evictDefaultSebConfig(exam);
this.clientConnectionDAO this.clientConnectionDAO
.getConnectionTokens(exam.id) .getConnectionTokens(exam.id)
.getOrElse(() -> Collections.emptyList()) .getOrElse(() -> Collections.emptyList())
@ -267,9 +282,9 @@ public class ExamSessionServiceImpl implements ExamSessionService {
// evict also cached ping record // evict also cached ping record
this.examSessionCacheService.evictPingRecord(token); this.examSessionCacheService.evictPingRecord(token);
}); });
} catch (final Exception e) {
log.error("Unexpected error while trying to flush cache for exam: ", exam, e); return exam;
} });
} }
} }

View file

@ -18,6 +18,7 @@ import org.springframework.boot.logging.LogLevel;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
@ -101,7 +102,9 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
clientAddress); clientAddress);
} }
checkExamRunning(examId); if (examId != null) {
checkExamIntegrity(examId);
}
// Create ClientConnection in status CONNECTION_REQUESTED for further processing // Create ClientConnection in status CONNECTION_REQUESTED for further processing
final String connectionToken = createToken(); final String connectionToken = createToken();
@ -133,6 +136,20 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
}); });
} }
private void checkExamIntegrity(final Long examId) {
// check Exam is running and not locked
checkExamRunning(examId);
if (this.examSessionService.isExamLocked(examId)) {
throw new APIConstraintViolationException(
"Exam is currently on update and locked for new SEB Client connections");
}
// if the cached Exam is not up to date anymore, we have to update the cache first
final Result<Exam> updateExamCache = this.examSessionService.updateExamCache(examId);
if (updateExamCache.hasError()) {
log.warn("Failed to update Exam-Cache for Exam: {}", examId);
}
}
@Override @Override
public Result<ClientConnection> updateClientConnection( public Result<ClientConnection> updateClientConnection(
final String connectionToken, final String connectionToken,
@ -170,6 +187,10 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
"ClientConnection integrity violation: client connection is not in expected state"); "ClientConnection integrity violation: client connection is not in expected state");
} }
if (examId != null) {
checkExamIntegrity(examId);
}
// userSessionId integrity check // userSessionId integrity check
if (userSessionId != null && if (userSessionId != null &&
clientConnection.userSessionId != null && clientConnection.userSessionId != null &&
@ -294,6 +315,8 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
.save(establishedClientConnection) .save(establishedClientConnection)
.getOrThrow(); .getOrThrow();
checkExamIntegrity(updatedClientConnection.examId);
// evict cached ClientConnection // evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken); this.examSessionCacheService.evictClientConnection(connectionToken);
// and load updated ClientConnection into cache // and load updated ClientConnection into cache

View file

@ -11,7 +11,6 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.Collection; import java.util.Collection;
import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -19,27 +18,18 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType; import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration; import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
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.sebconfig.ConfigurationChangedEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile @WebServiceProfile
@ -48,9 +38,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
public class ConfigurationController extends ReadonlyEntityController<Configuration, Configuration> { public class ConfigurationController extends ReadonlyEntityController<Configuration, Configuration> {
private final ConfigurationDAO configurationDAO; private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO; private final ExamConfigUpdateService examConfigUpdateService;
private final ApplicationEventPublisher applicationEventPublisher;
private final ExamSessionService examSessionService;
protected ConfigurationController( protected ConfigurationController(
final AuthorizationService authorization, final AuthorizationService authorization,
@ -59,9 +47,7 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
final UserActivityLogDAO userActivityLogDAO, final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService, final PaginationService paginationService,
final BeanValidationService beanValidationService, final BeanValidationService beanValidationService,
final ApplicationEventPublisher applicationEventPublisher, final ExamConfigUpdateService examConfigUpdateService) {
final ExamSessionService examSessionService,
final ExamConfigurationMapDAO examConfigurationMapDAO) {
super(authorization, super(authorization,
bulkActionService, bulkActionService,
@ -71,9 +57,7 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
beanValidationService); beanValidationService);
this.configurationDAO = entityDAO; this.configurationDAO = entityDAO;
this.applicationEventPublisher = applicationEventPublisher; this.examConfigUpdateService = examConfigUpdateService;
this.examSessionService = examSessionService;
this.examConfigurationMapDAO = examConfigurationMapDAO;
} }
@RequestMapping( @RequestMapping(
@ -81,14 +65,13 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
method = RequestMethod.POST, method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE) produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Configuration saveToHistory(@PathVariable final String modelId) { public Configuration saveToHistory(@PathVariable final Long modelId) {
return this.entityDAO.byModelId(modelId) return this.entityDAO.byPK(modelId)
.flatMap(this.authorization::checkModify) .flatMap(this.authorization::checkModify)
.flatMap(this::checkRunningExamIntegrity) .flatMap(this.examConfigUpdateService::processSEBExamConfigurationChange)
.flatMap(config -> this.configurationDAO.saveToHistory(config.configurationNodeId)) .onError(t -> this.examConfigUpdateService.forceReleaseUpdateLocks(modelId))
.flatMap(this.userActivityLogDAO::logSaveToHistory) .flatMap(this.userActivityLogDAO::logSaveToHistory)
.flatMap(this::publishConfigChanged)
.getOrThrow(); .getOrThrow();
} }
@ -103,7 +86,6 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
.flatMap(this.authorization::checkModify) .flatMap(this.authorization::checkModify)
.flatMap(config -> this.configurationDAO.undo(config.configurationNodeId)) .flatMap(config -> this.configurationDAO.undo(config.configurationNodeId))
.flatMap(this.userActivityLogDAO::logUndo) .flatMap(this.userActivityLogDAO::logUndo)
.flatMap(this::publishConfigChanged)
.getOrThrow(); .getOrThrow();
} }
@ -119,7 +101,6 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
return this.entityDAO.byModelId(modelId) return this.entityDAO.byModelId(modelId)
.flatMap(this.authorization::checkModify) .flatMap(this.authorization::checkModify)
.flatMap(config -> this.configurationDAO.restoreToVersion(configurationNodeId, config.getId())) .flatMap(config -> this.configurationDAO.restoreToVersion(configurationNodeId, config.getId()))
.flatMap(this::publishConfigChanged)
.getOrThrow(); .getOrThrow();
} }
@ -133,55 +114,4 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
return ConfigurationRecordDynamicSqlSupport.configurationRecord; return ConfigurationRecordDynamicSqlSupport.configurationRecord;
} }
// NOTE: This will not properly work within a distributed setup since other instances
// are not notified about the configuration change
// TODO: find a way to manage the notification of a changed configuration that works
// also on distributed environments. For example use the database to store configuration
// changed information and check before getting a configuration from cache if it is still valid
private Result<Configuration> publishConfigChanged(final Configuration config) {
this.applicationEventPublisher.publishEvent(new ConfigurationChangedEvent(config.id));
return Result.of(config);
}
private Result<Configuration> checkRunningExamIntegrity(final Configuration config) {
// check if the configuration is attached to an exam
final long activeConnections = this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(config.configurationNodeId)
.getOrThrow()
.stream()
.flatMap(examId -> {
return this.examSessionService
.getConnectionData(examId)
.getOrThrow()
.stream();
})
.filter(this::isActiveConnection)
.count();
if (activeConnections > 0) {
throw new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,
"Integrity violation: There are currently active SEB Client connection.");
} else {
return Result.of(config);
}
}
private boolean isActiveConnection(final ClientConnectionData connection) {
if (connection.clientConnection.status == ConnectionStatus.ESTABLISHED
|| connection.clientConnection.status == ConnectionStatus.AUTHENTICATED) {
return true;
}
if (connection.clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
final Long creationTime = connection.clientConnection.getCreationTime();
final long millisecondsNow = Utils.getMillisecondsNow();
if (millisecondsNow - creationTime < 30 * Constants.SECOND_IN_MILLIS) {
return true;
}
}
return false;
}
} }

View file

@ -27,8 +27,9 @@ sebserver.webservice.http.redirect.gui=/gui
sebserver.webservice.api.admin.endpoint=/admin-api/v1 sebserver.webservice.api.admin.endpoint=/admin-api/v1
sebserver.webservice.api.admin.accessTokenValiditySeconds=3600 sebserver.webservice.api.admin.accessTokenValiditySeconds=3600
sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1 sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1
sebserver.webservice.api.exam.time-prefix=3600000 sebserver.webservice.api.exam.update-interval=1 * * * * *
sebserver.webservice.api.exam.time-suffix=3600000 sebserver.webservice.api.exam.time-prefix=0
sebserver.webservice.api.exam.time-suffix=0
sebserver.webservice.api.exam.endpoint=/exam-api sebserver.webservice.api.exam.endpoint=/exam-api
sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam.endpoint}/discovery sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam.endpoint}/discovery
sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1 sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1

View file

@ -448,6 +448,7 @@ sebserver.examconfig.action.modify.properties=Edit Configuration
sebserver.examconfig.action.save=Save sebserver.examconfig.action.save=Save
sebserver.examconfig.action.saveToHistory=Save / Publish sebserver.examconfig.action.saveToHistory=Save / Publish
sebserver.examconfig.action.saveToHistory.success=Successfully saved in history sebserver.examconfig.action.saveToHistory.success=Successfully saved in history
sebserver.examconfig.action.saveToHistory.integrity-violation=There is currently at least one running Exam with active SEB client connections that uses this Configuration.<br/>Modify of a configuration that is currently in use would lead to inconsistency and is therefore not allowed.<br/><br/>Please make sure that the configuration is not in use before applying changes.
sebserver.examconfig.action.undo=Undo sebserver.examconfig.action.undo=Undo
sebserver.examconfig.action.undo.success=Successfully reverted to last saved state sebserver.examconfig.action.undo.success=Successfully reverted to last saved state
sebserver.examconfig.action.copy=Copy Configuration sebserver.examconfig.action.copy=Copy Configuration
@ -486,6 +487,8 @@ sebserver.examconfig.props.form.views.security=Security
sebserver.examconfig.props.form.views.registry=Registry sebserver.examconfig.props.form.views.registry=Registry
sebserver.examconfig.props.form.views.hooked_keys=Hooked Keys sebserver.examconfig.props.form.views.hooked_keys=Hooked Keys
sebserver.examconfig.props.label.hashedAdminPassword=Administrator password sebserver.examconfig.props.label.hashedAdminPassword=Administrator password
sebserver.examconfig.props.label.hashedAdminPassword.confirm=Confirm password sebserver.examconfig.props.label.hashedAdminPassword.confirm=Confirm password
sebserver.examconfig.props.label.allowQuit=Allow user to quit SEB sebserver.examconfig.props.label.allowQuit=Allow user to quit SEB

View file

@ -706,7 +706,8 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
null, null, null, null,
Utils.immutableCollectionOf(userId), Utils.immutableCollectionOf(userId),
ExamStatus.RUNNING, ExamStatus.RUNNING,
true); true,
null);
final Result<Exam> savedExamResult = restService final Result<Exam> savedExamResult = restService
.getBuilder(SaveExam.class) .getBuilder(SaveExam.class)

View file

@ -65,7 +65,8 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
exam.owner, exam.owner,
Arrays.asList("user5"), Arrays.asList("user5"),
null, null,
true)) true,
null))
.withExpectedStatus(HttpStatus.OK) .withExpectedStatus(HttpStatus.OK)
.getAsObject(new TypeReference<Exam>() { .getAsObject(new TypeReference<Exam>() {
}); });
@ -94,7 +95,8 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
exam.owner, exam.owner,
Arrays.asList("user2"), Arrays.asList("user2"),
null, null,
true)) true,
null))
.withExpectedStatus(HttpStatus.BAD_REQUEST) .withExpectedStatus(HttpStatus.BAD_REQUEST)
.getAsObject(new TypeReference<List<APIMessage>>() { .getAsObject(new TypeReference<List<APIMessage>>() {
}); });

View file

@ -59,7 +59,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester {
}); });
assertNotNull(quizzes); assertNotNull(quizzes);
assertTrue(quizzes.content.size() == 7); assertTrue(quizzes.content.size() == 8);
// for the inactive LmsSetup we should'nt get any quizzes // for the inactive LmsSetup we should'nt get any quizzes
quizzes = new RestAPITestHelper() quizzes = new RestAPITestHelper()
@ -109,7 +109,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester {
}); });
assertNotNull(quizzes); assertNotNull(quizzes);
assertTrue(quizzes.content.size() == 7); assertTrue(quizzes.content.size() == 8);
// but for the now active lmsSetup2 we should get the quizzes // but for the now active lmsSetup2 we should get the quizzes
quizzes = new RestAPITestHelper() quizzes = new RestAPITestHelper()
@ -120,7 +120,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester {
}); });
assertNotNull(quizzes); assertNotNull(quizzes);
assertTrue(quizzes.content.size() == 7); assertTrue(quizzes.content.size() == 8);
} }
@Test @Test