SEBSERV-73 finished SEB Exam config update for running exams
This commit is contained in:
parent
af6ebf7666
commit
9b9aa4625d
24 changed files with 607 additions and 206 deletions
|
@ -17,6 +17,7 @@ import javax.validation.constraints.NotNull;
|
|||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
@ -47,7 +48,8 @@ public final class Exam implements GrantEntity {
|
|||
null,
|
||||
null,
|
||||
ExamStatus.FINISHED,
|
||||
Boolean.FALSE);
|
||||
Boolean.FALSE,
|
||||
null);
|
||||
|
||||
public static final String FILTER_ATTR_TYPE = "type";
|
||||
public static final String FILTER_ATTR_STATUS = "status";
|
||||
|
@ -115,6 +117,9 @@ public final class Exam implements GrantEntity {
|
|||
@JsonProperty(EXAM.ATTR_ACTIVE)
|
||||
public final Boolean active;
|
||||
|
||||
@JsonProperty(EXAM.ATTR_LASTUPDATE)
|
||||
public final String lastUpdate;
|
||||
|
||||
@JsonCreator
|
||||
public Exam(
|
||||
@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_SUPPORTER) final Collection<String> supporter,
|
||||
@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.institutionId = institutionId;
|
||||
|
@ -145,8 +151,9 @@ public final class Exam implements GrantEntity {
|
|||
this.type = type;
|
||||
this.quitPassword = quitPassword;
|
||||
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.lastUpdate = lastUpdate;
|
||||
|
||||
this.supporter = (supporter != null)
|
||||
? 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.quitPassword = mapper.getString(EXAM.ATTR_QUIT_PASSWORD);
|
||||
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.supporter = mapper.getStringSet(EXAM.ATTR_SUPPORTER);
|
||||
this.lastUpdate = null;
|
||||
}
|
||||
|
||||
public Exam(final QuizData quizzData) {
|
||||
|
@ -189,9 +200,10 @@ public final class Exam implements GrantEntity {
|
|||
this.type = null;
|
||||
this.quitPassword = 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.supporter = null;
|
||||
this.lastUpdate = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -326,4 +338,17 @@ public final class Exam implements GrantEntity {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ import org.springframework.stereotype.Component;
|
|||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessageError;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
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.i18n.LocTextKey;
|
||||
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.TemplateComposer;
|
||||
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";
|
||||
private static final String KEY_UNDO_SUCCESS =
|
||||
"sebserver.examconfig.action.undo.success";
|
||||
|
||||
private static final LocTextKey TITLE_TEXT_KEY =
|
||||
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 RestService restService;
|
||||
private final CurrentUser currentUser;
|
||||
|
@ -150,8 +155,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
|
|||
this.restService.getBuilder(SaveExamConfigHistory.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, configuration.getModelId())
|
||||
.call()
|
||||
.onError(pageContext::notifyError)
|
||||
.getOrThrow();
|
||||
.onError(t -> notifyErrorOnSave(t, pageContext));
|
||||
return action;
|
||||
})
|
||||
.withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS)
|
||||
|
@ -164,7 +168,6 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
|
|||
this.restService.getBuilder(SebExamConfigUndo.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, configuration.getModelId())
|
||||
.call()
|
||||
.onError(pageContext::notifyError)
|
||||
.getOrThrow();
|
||||
return action;
|
||||
})
|
||||
|
@ -175,9 +178,7 @@ public class SebExamConfigSettingsForm implements TemplateComposer {
|
|||
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)
|
||||
.withEntityKey(entityKey)
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.publish()
|
||||
|
||||
;
|
||||
.publish();
|
||||
|
||||
} catch (final RuntimeException 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -303,7 +303,6 @@ public class PageContextImpl implements PageContext {
|
|||
|
||||
@Override
|
||||
public <T> T notifyError(final Throwable error) {
|
||||
log.error("Unexpected error: ", error);
|
||||
notifyError(error.getMessage(), error);
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ public interface ExamConfigurationMapDAO extends
|
|||
* @param examId The Exam identifier
|
||||
* @return ConfigurationNode identifier of the default Exam Configuration of
|
||||
* the Exam with specified identifier */
|
||||
Result<Long> getDefaultConfigurationForExam(Long examId);
|
||||
Result<Long> getDefaultConfigurationNode(Long examId);
|
||||
|
||||
/** Get the ConfigurationNode identifier of the Exam Configuration of
|
||||
* the Exam for a specified user identifier.
|
||||
|
@ -47,7 +47,7 @@ public interface ExamConfigurationMapDAO extends
|
|||
* @param userId the user identifier
|
||||
* @return ConfigurationNode identifier of the Exam Configuration of
|
||||
* 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.
|
||||
*
|
||||
|
|
|
@ -44,11 +44,13 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
|||
|
||||
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);
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.stream.Collectors;
|
|||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
|
@ -148,7 +149,7 @@ public class ConfigurationDAOImpl implements ConfigurationDAO {
|
|||
.execute();
|
||||
Collections.sort(
|
||||
configs,
|
||||
(c1, c2) -> c1.getVersionDate().compareTo(c2.getVersionDate()));
|
||||
(c1, c2) -> c1.getVersionDate().compareTo(c2.getVersionDate()) * -1);
|
||||
final ConfigurationRecord configurationRecord = configs.get(0);
|
||||
return configurationRecord;
|
||||
}).flatMap(ConfigurationDAOImpl::toDomainModel);
|
||||
|
@ -177,7 +178,7 @@ public class ConfigurationDAOImpl implements ConfigurationDAO {
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public Result<Configuration> saveToHistory(final Long configurationNodeId) {
|
||||
return this.configurationDAOBatchService
|
||||
.saveToHistory(configurationNodeId)
|
||||
|
|
|
@ -173,7 +173,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
|
|||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Long> getDefaultConfigurationForExam(final Long examId) {
|
||||
public Result<Long> getDefaultConfigurationNode(final Long examId) {
|
||||
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
|
||||
.selectByExample()
|
||||
.where(
|
||||
|
@ -190,7 +190,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
|
|||
}
|
||||
|
||||
@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
|
||||
.selectByExample()
|
||||
.where(
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||
import org.joda.time.DateTime;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
|
@ -172,13 +173,14 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
(exam.supporter != null)
|
||||
? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR)
|
||||
: null,
|
||||
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
|
||||
(exam.type != null) ? exam.type.name() : null,
|
||||
exam.quitPassword,
|
||||
null, // browser keys
|
||||
null, // status
|
||||
(exam.status != null) ? exam.status.name() : null,
|
||||
null, // updating
|
||||
null, // lastUpdate
|
||||
BooleanUtils.toIntegerObject(exam.active));
|
||||
null // active
|
||||
);
|
||||
|
||||
this.examRecordMapper.updateByPrimaryKeySelective(examRecord);
|
||||
return this.examRecordMapper.selectByPrimaryKey(exam.id);
|
||||
|
@ -230,8 +232,8 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
|
||||
null, // quitPassword
|
||||
null, // browser keys
|
||||
null, // status
|
||||
null, // updating
|
||||
(exam.status != null) ? exam.status.name() : ExamStatus.UP_COMING.name(),
|
||||
BooleanUtils.toInteger(false),
|
||||
null, // lastUpdate
|
||||
BooleanUtils.toInteger(true));
|
||||
|
||||
|
@ -329,8 +331,8 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Result<Exam> startUpdate(final Long examId, final String update) {
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public Result<Exam> placeLock(final Long examId, final String update) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final ExamRecord examRec = this.recordById(examId)
|
||||
|
@ -357,8 +359,8 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Result<Exam> endUpdate(final Long examId, final String update) {
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public Result<Exam> releaseLock(final Long examId, final String update) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final ExamRecord examRec = this.recordById(examId)
|
||||
|
@ -386,9 +388,29 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
.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
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Boolean> isUpdating(final Long examId) {
|
||||
public Result<Boolean> isLocked(final Long examId) {
|
||||
return this.recordById(examId)
|
||||
.map(rec -> BooleanUtils.toBooleanObject(rec.getUpdating()));
|
||||
}
|
||||
|
@ -397,7 +419,13 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
@Transactional(readOnly = true)
|
||||
public Result<Boolean> upToDate(final Long examId, final String lastUpdate) {
|
||||
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
|
||||
|
@ -588,7 +616,8 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
record.getOwner(),
|
||||
supporter,
|
||||
status,
|
||||
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : 0));
|
||||
BooleanUtils.toBooleanObject((quizData != null) ? record.getActive() : 0),
|
||||
record.getLastupdate());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,12 @@ import java.util.Set;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
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 ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
|
@ -53,26 +56,35 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
|||
final LmsType lmsType = lmsSetup.getLmsType();
|
||||
this.mockups = new ArrayList<>();
|
||||
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/"));
|
||||
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/"));
|
||||
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/"));
|
||||
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/"));
|
||||
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/"));
|
||||
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/"));
|
||||
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/"));
|
||||
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -147,11 +147,11 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
|
|||
}
|
||||
|
||||
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) {
|
||||
return this.examConfigurationMapDAO.getUserConfigurationIdForExam(examId, userId);
|
||||
return this.examConfigurationMapDAO.getUserConfigurationNodeId(examId, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -12,15 +12,12 @@ import java.io.OutputStream;
|
|||
import java.util.Collection;
|
||||
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.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
||||
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.FilterMap;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationChangedEvent;
|
||||
|
||||
/** A Service to handle running exam sessions */
|
||||
public interface ExamSessionService {
|
||||
|
@ -47,6 +44,12 @@ public interface ExamSessionService {
|
|||
* @return true if an Exam is currently running */
|
||||
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.
|
||||
* 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
|
||||
|
@ -99,7 +102,17 @@ public interface ExamSessionService {
|
|||
* of a running exam */
|
||||
Result<Collection<ClientConnectionData>> getConnectionData(Long examId);
|
||||
|
||||
@EventListener(ConfigurationChangedEvent.class)
|
||||
void updateExamConfigCache(ConfigurationChangedEvent configChanged);
|
||||
/** Use this to check if the current cached running exam is up to date
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -100,8 +100,7 @@ public class ExamSessionCacheService {
|
|||
|
||||
@CacheEvict(
|
||||
cacheNames = CACHE_NAME_RUNNING_EXAM,
|
||||
key = "#exam.id",
|
||||
condition = "#target.isRunning(#result)")
|
||||
key = "#exam.id")
|
||||
public Exam evict(final Exam exam) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
|
@ -111,7 +110,7 @@ public class ExamSessionCacheService {
|
|||
return exam;
|
||||
}
|
||||
|
||||
boolean isRunning(final Exam exam) {
|
||||
public boolean isRunning(final Exam exam) {
|
||||
if (exam == null) {
|
||||
return false;
|
||||
}
|
||||
|
@ -131,7 +130,6 @@ public class ExamSessionCacheService {
|
|||
default: {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,33 +164,31 @@ public class ExamSessionCacheService {
|
|||
|
||||
@Cacheable(
|
||||
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
|
||||
key = "#examId",
|
||||
key = "#exam.id",
|
||||
unless = "#result == null")
|
||||
public InMemorySebConfig getDefaultSebConfigForExam(final Long examId) {
|
||||
final Exam runningExam = this.getRunningExam(examId);
|
||||
|
||||
public InMemorySebConfig getDefaultSebConfigForExam(final Exam exam) {
|
||||
try {
|
||||
|
||||
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
|
||||
final Long configId = this.sebExamConfigService.exportForExam(
|
||||
byteOut,
|
||||
runningExam.institutionId,
|
||||
examId);
|
||||
exam.institutionId,
|
||||
exam.id);
|
||||
|
||||
return new InMemorySebConfig(configId, runningExam.id, byteOut.toByteArray());
|
||||
return new InMemorySebConfig(configId, exam.id, byteOut.toByteArray());
|
||||
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@CacheEvict(
|
||||
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
|
||||
key = "#examId")
|
||||
public void evictDefaultSebConfig(final Long examId) {
|
||||
key = "#exam.id")
|
||||
public void evictDefaultSebConfig(final Exam exam) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||
|
||||
@Service
|
||||
public class ExamSessionControlTask {
|
||||
class ExamSessionControlTask {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExamSessionControlTask.class);
|
||||
|
||||
|
@ -47,20 +47,19 @@ public class ExamSessionControlTask {
|
|||
this.examDAO = examDAO;
|
||||
this.examTimePrefix = examTimePrefix;
|
||||
this.examTimeSuffix = examTimeSuffix;
|
||||
|
||||
this.updatePrefix = webserviceInfo.getHostAddress()
|
||||
+ "_" + webserviceInfo.getServerPort() + "_";
|
||||
}
|
||||
|
||||
@Async
|
||||
@Scheduled(cron = "1 * * * * *")
|
||||
@Scheduled(cron = "${sebserver.webservice.api.exam.update-interval:1 * * * * *}")
|
||||
@Transactional
|
||||
public void execTask() {
|
||||
|
||||
final String updateId = createUpdateId();
|
||||
final String updateId = this.createUpdateId();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Run ExamControlTask. Update Id: {}", updateId);
|
||||
log.debug("Run exam runtime update task with Id: {}", updateId);
|
||||
}
|
||||
|
||||
controlStart(updateId);
|
||||
|
@ -84,7 +83,9 @@ public class ExamSessionControlTask {
|
|||
.map(exam -> this.setRunning(exam, updateId))
|
||||
.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) {
|
||||
log.error("Unexpected error while trying to update exams: ", e);
|
||||
|
@ -97,15 +98,15 @@ public class ExamSessionControlTask {
|
|||
final DateTime now = DateTime.now(DateTimeZone.UTC);
|
||||
if (exam.getStatus() == ExamStatus.UP_COMING
|
||||
&& exam.endTime.plus(this.examTimeSuffix).isBefore(now)) {
|
||||
return setRunning(exam, createUpdateId());
|
||||
return setRunning(exam, this.createUpdateId());
|
||||
} else {
|
||||
return exam;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Result<Exam> setRunning(final Exam exam) {
|
||||
return Result.tryCatch(() -> setRunning(exam, createUpdateId()));
|
||||
public String createUpdateId() {
|
||||
return this.updatePrefix + Utils.getMillisecondsNow();
|
||||
}
|
||||
|
||||
private Exam setRunning(final Exam exam, final String updateId) {
|
||||
|
@ -114,11 +115,11 @@ public class ExamSessionControlTask {
|
|||
}
|
||||
|
||||
return this.examDAO
|
||||
.startUpdate(exam.id, updateId)
|
||||
.placeLock(exam.id, updateId)
|
||||
.flatMap(e -> this.examDAO.save(new Exam(
|
||||
exam.id,
|
||||
ExamStatus.RUNNING)))
|
||||
.flatMap(e -> this.examDAO.endUpdate(e.id, updateId))
|
||||
.flatMap(e -> this.examDAO.releaseLock(e.id, updateId))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
|
@ -138,7 +139,9 @@ public class ExamSessionControlTask {
|
|||
.map(exam -> this.setFinished(exam, updateId))
|
||||
.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) {
|
||||
log.error("Unexpected error while trying to update exams: ", e);
|
||||
|
@ -151,16 +154,12 @@ public class ExamSessionControlTask {
|
|||
}
|
||||
|
||||
return this.examDAO
|
||||
.startUpdate(exam.id, updateId)
|
||||
.placeLock(exam.id, updateId)
|
||||
.flatMap(e -> this.examDAO.save(new Exam(
|
||||
exam.id,
|
||||
ExamStatus.FINISHED)))
|
||||
.flatMap(e -> this.examDAO.endUpdate(e.id, updateId))
|
||||
.flatMap(e -> this.examDAO.releaseLock(e.id, updateId))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
private String createUpdateId() {
|
||||
return this.updatePrefix + Utils.getMillisecondsNow();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,12 +17,12 @@ import java.util.NoSuchElementException;
|
|||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
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.FilterMap;
|
||||
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;
|
||||
|
||||
@Lazy
|
||||
|
@ -92,7 +91,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
}
|
||||
|
||||
// check SEB configuration
|
||||
this.examConfigurationMapDAO.getDefaultConfigurationForExam(examId)
|
||||
this.examConfigurationMapDAO.getDefaultConfigurationNode(examId)
|
||||
.get(t -> {
|
||||
result.add(ErrorMessage.EXAM_CONSISTANCY_VALIDATION_CONFIG.of(exam.getModelId()));
|
||||
return null;
|
||||
|
@ -116,6 +115,17 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
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
|
||||
public Result<Exam> getRunningExam(final Long examId) {
|
||||
if (log.isTraceEnabled()) {
|
||||
|
@ -123,6 +133,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
}
|
||||
|
||||
final Exam exam = this.examSessionCacheService.getRunningExam(examId);
|
||||
|
||||
if (this.examSessionCacheService.isRunning(exam)) {
|
||||
if (log.isTraceEnabled()) {
|
||||
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");
|
||||
}
|
||||
|
||||
final Exam exam = this.getRunningExam(connection.examId)
|
||||
.getOrThrow();
|
||||
|
||||
final InMemorySebConfig sebConfigForExam = this.examSessionCacheService
|
||||
.getDefaultSebConfigForExam(connection.examId);
|
||||
.getDefaultSebConfigForExam(exam);
|
||||
|
||||
if (sebConfigForExam == null) {
|
||||
log.error("Failed to get and cache InMemorySebConfig for connection: {}", connection);
|
||||
|
@ -241,23 +255,24 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
}
|
||||
|
||||
@Override
|
||||
@EventListener(ConfigurationChangedEvent.class)
|
||||
public void updateExamConfigCache(final ConfigurationChangedEvent configChanged) {
|
||||
public Result<Exam> updateExamCache(final Long examId) {
|
||||
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()) {
|
||||
log.debug("Flush exam config cache for configuration: {}", configChanged.configurationId);
|
||||
if (!BooleanUtils.toBoolean(isUpToDate)) {
|
||||
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) {
|
||||
try {
|
||||
@Override
|
||||
public Result<Exam> flushCache(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
this.examSessionCacheService.evict(exam);
|
||||
this.examSessionCacheService.evictDefaultSebConfig(exam.id);
|
||||
this.examSessionCacheService.evictDefaultSebConfig(exam);
|
||||
this.clientConnectionDAO
|
||||
.getConnectionTokens(exam.id)
|
||||
.getOrElse(() -> Collections.emptyList())
|
||||
|
@ -267,9 +282,9 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
// evict also cached ping record
|
||||
this.examSessionCacheService.evictPingRecord(token);
|
||||
});
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while trying to flush cache for exam: ", exam, e);
|
||||
}
|
||||
|
||||
return exam;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import org.springframework.boot.logging.LogLevel;
|
|||
import org.springframework.context.annotation.Lazy;
|
||||
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.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
|
||||
|
@ -101,7 +102,9 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
|
|||
clientAddress);
|
||||
}
|
||||
|
||||
checkExamRunning(examId);
|
||||
if (examId != null) {
|
||||
checkExamIntegrity(examId);
|
||||
}
|
||||
|
||||
// Create ClientConnection in status CONNECTION_REQUESTED for further processing
|
||||
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
|
||||
public Result<ClientConnection> updateClientConnection(
|
||||
final String connectionToken,
|
||||
|
@ -170,6 +187,10 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
|
|||
"ClientConnection integrity violation: client connection is not in expected state");
|
||||
}
|
||||
|
||||
if (examId != null) {
|
||||
checkExamIntegrity(examId);
|
||||
}
|
||||
|
||||
// userSessionId integrity check
|
||||
if (userSessionId != null &&
|
||||
clientConnection.userSessionId != null &&
|
||||
|
@ -294,6 +315,8 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
|
|||
.save(establishedClientConnection)
|
||||
.getOrThrow();
|
||||
|
||||
checkExamIntegrity(updatedClientConnection.examId);
|
||||
|
||||
// evict cached ClientConnection
|
||||
this.examSessionCacheService.evictClientConnection(connectionToken);
|
||||
// and load updated ClientConnection into cache
|
||||
|
|
|
@ -11,7 +11,6 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
|
|||
import java.util.Collection;
|
||||
|
||||
import org.mybatis.dynamic.sql.SqlTable;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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.RestController;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
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.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.datalayer.batis.mapper.ConfigurationRecordDynamicSqlSupport;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
|
||||
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.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.sebconfig.ConfigurationChangedEvent;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
|
||||
|
||||
@WebServiceProfile
|
||||
|
@ -48,9 +38,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
|
|||
public class ConfigurationController extends ReadonlyEntityController<Configuration, Configuration> {
|
||||
|
||||
private final ConfigurationDAO configurationDAO;
|
||||
private final ExamConfigurationMapDAO examConfigurationMapDAO;
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
private final ExamSessionService examSessionService;
|
||||
private final ExamConfigUpdateService examConfigUpdateService;
|
||||
|
||||
protected ConfigurationController(
|
||||
final AuthorizationService authorization,
|
||||
|
@ -59,9 +47,7 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
|
|||
final UserActivityLogDAO userActivityLogDAO,
|
||||
final PaginationService paginationService,
|
||||
final BeanValidationService beanValidationService,
|
||||
final ApplicationEventPublisher applicationEventPublisher,
|
||||
final ExamSessionService examSessionService,
|
||||
final ExamConfigurationMapDAO examConfigurationMapDAO) {
|
||||
final ExamConfigUpdateService examConfigUpdateService) {
|
||||
|
||||
super(authorization,
|
||||
bulkActionService,
|
||||
|
@ -71,9 +57,7 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
|
|||
beanValidationService);
|
||||
|
||||
this.configurationDAO = entityDAO;
|
||||
this.applicationEventPublisher = applicationEventPublisher;
|
||||
this.examSessionService = examSessionService;
|
||||
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
||||
this.examConfigUpdateService = examConfigUpdateService;
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
|
@ -81,14 +65,13 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
|
|||
method = RequestMethod.POST,
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_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::checkRunningExamIntegrity)
|
||||
.flatMap(config -> this.configurationDAO.saveToHistory(config.configurationNodeId))
|
||||
.flatMap(this.examConfigUpdateService::processSEBExamConfigurationChange)
|
||||
.onError(t -> this.examConfigUpdateService.forceReleaseUpdateLocks(modelId))
|
||||
.flatMap(this.userActivityLogDAO::logSaveToHistory)
|
||||
.flatMap(this::publishConfigChanged)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
|
@ -103,7 +86,6 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
|
|||
.flatMap(this.authorization::checkModify)
|
||||
.flatMap(config -> this.configurationDAO.undo(config.configurationNodeId))
|
||||
.flatMap(this.userActivityLogDAO::logUndo)
|
||||
.flatMap(this::publishConfigChanged)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
|
@ -119,7 +101,6 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
|
|||
return this.entityDAO.byModelId(modelId)
|
||||
.flatMap(this.authorization::checkModify)
|
||||
.flatMap(config -> this.configurationDAO.restoreToVersion(configurationNodeId, config.getId()))
|
||||
.flatMap(this::publishConfigChanged)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
|
@ -133,55 +114,4 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,8 +27,9 @@ sebserver.webservice.http.redirect.gui=/gui
|
|||
sebserver.webservice.api.admin.endpoint=/admin-api/v1
|
||||
sebserver.webservice.api.admin.accessTokenValiditySeconds=3600
|
||||
sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1
|
||||
sebserver.webservice.api.exam.time-prefix=3600000
|
||||
sebserver.webservice.api.exam.time-suffix=3600000
|
||||
sebserver.webservice.api.exam.update-interval=1 * * * * *
|
||||
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.discovery=${sebserver.webservice.api.exam.endpoint}/discovery
|
||||
sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1
|
||||
|
|
|
@ -448,6 +448,7 @@ sebserver.examconfig.action.modify.properties=Edit Configuration
|
|||
sebserver.examconfig.action.save=Save
|
||||
sebserver.examconfig.action.saveToHistory=Save / Publish
|
||||
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.success=Successfully reverted to last saved state
|
||||
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.hooked_keys=Hooked Keys
|
||||
|
||||
|
||||
|
||||
sebserver.examconfig.props.label.hashedAdminPassword=Administrator password
|
||||
sebserver.examconfig.props.label.hashedAdminPassword.confirm=Confirm password
|
||||
sebserver.examconfig.props.label.allowQuit=Allow user to quit SEB
|
||||
|
|
|
@ -706,7 +706,8 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
|||
null, null,
|
||||
Utils.immutableCollectionOf(userId),
|
||||
ExamStatus.RUNNING,
|
||||
true);
|
||||
true,
|
||||
null);
|
||||
|
||||
final Result<Exam> savedExamResult = restService
|
||||
.getBuilder(SaveExam.class)
|
||||
|
|
|
@ -65,7 +65,8 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
|
|||
exam.owner,
|
||||
Arrays.asList("user5"),
|
||||
null,
|
||||
true))
|
||||
true,
|
||||
null))
|
||||
.withExpectedStatus(HttpStatus.OK)
|
||||
.getAsObject(new TypeReference<Exam>() {
|
||||
});
|
||||
|
@ -94,7 +95,8 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
|
|||
exam.owner,
|
||||
Arrays.asList("user2"),
|
||||
null,
|
||||
true))
|
||||
true,
|
||||
null))
|
||||
.withExpectedStatus(HttpStatus.BAD_REQUEST)
|
||||
.getAsObject(new TypeReference<List<APIMessage>>() {
|
||||
});
|
||||
|
|
|
@ -59,7 +59,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester {
|
|||
});
|
||||
|
||||
assertNotNull(quizzes);
|
||||
assertTrue(quizzes.content.size() == 7);
|
||||
assertTrue(quizzes.content.size() == 8);
|
||||
|
||||
// for the inactive LmsSetup we should'nt get any quizzes
|
||||
quizzes = new RestAPITestHelper()
|
||||
|
@ -109,7 +109,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester {
|
|||
});
|
||||
|
||||
assertNotNull(quizzes);
|
||||
assertTrue(quizzes.content.size() == 7);
|
||||
assertTrue(quizzes.content.size() == 8);
|
||||
|
||||
// but for the now active lmsSetup2 we should get the quizzes
|
||||
quizzes = new RestAPITestHelper()
|
||||
|
@ -120,7 +120,7 @@ public class QuizDataTest extends AdministrationAPIIntegrationTester {
|
|||
});
|
||||
|
||||
assertNotNull(quizzes);
|
||||
assertTrue(quizzes.content.size() == 7);
|
||||
assertTrue(quizzes.content.size() == 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue