SEBSERV-73 seb exam config add/remove on running exam

This commit is contained in:
anhefti 2019-11-01 20:50:42 +01:00
parent d1f80baa87
commit 810b6dc8c2
5 changed files with 130 additions and 37 deletions

View file

@ -9,8 +9,10 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.util.Collection;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ExamConfigUpdateService {
@ -48,9 +50,11 @@ public interface ExamConfigUpdateService {
*
* @param configurationNodeId the SEB Configuration node identifier
* @return Result refer to a list of involved and updated Exam identifiers */
Result<Collection<Long>> processSEBExamConfigurationChange(Long configurationNodeId);
Result<Collection<Long>> processExamConfigurationChange(Long configurationNodeId);
Result<Long> processSEBExamConfigurationAttachmentChange(Long examId);
<T> Result<T> processExamConfigurationMappingChange(
ExamConfigurationMap mapping,
Function<ExamConfigurationMap, Result<T>> changeAction);
/** Use this to force a release of update-locks for all Exams that has the specified
* SEB Exam Configuration attached.

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@ -24,6 +25,7 @@ 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.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
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;
@ -73,7 +75,7 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
// evict each Exam from cache and release the update-lock on DB
@Override
@Transactional
public Result<Collection<Long>> processSEBExamConfigurationChange(final Long configurationNodeId) {
public Result<Collection<Long>> processExamConfigurationChange(final Long configurationNodeId) {
final String updateId = this.examUpdateHandler.createUpdateId();
@ -170,21 +172,76 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
}
@Override
public Result<Long> processSEBExamConfigurationAttachmentChange(final Long examId) {
return this.examDAO.byPK(examId)
@Transactional
public <T> Result<T> processExamConfigurationMappingChange(
final ExamConfigurationMap mapping,
final Function<ExamConfigurationMap, Result<T>> changeAction) {
return this.examDAO.byPK(mapping.examId)
.map(exam -> {
// if the exam is not currently running just apply the action
if (exam.status != ExamStatus.RUNNING) {
return examId;
return changeAction
.apply(mapping)
.getOrThrow();
}
// TODO Lock??
// TODO flush cache
// TODO update seb restriction if on
// TODO unlock?
// if the exam is running...
final String updateId = this.examUpdateHandler.createUpdateId();
if (log.isDebugEnabled()) {
log.debug("Process SEB Exam Configuration mapping update for: {} with update-id {}",
mapping,
updateId);
}
return examId;
});
// check if there are no active client connections for this exam
checkActiveClientConnections(exam);
// lock the exam
this.examDAO.placeLock(exam.id, updateId)
.getOrThrow();
// check again if there are no new active client connections in the meantime
checkActiveClientConnections(exam);
// apply the referenced change action
final T result = changeAction.apply(mapping)
.getOrThrow();
// flush the exam cache
this.examSessionService.flushCache(exam)
.getOrThrow();
// update seb client restriction if the feature is activated
if (exam.lmsSebRestriction) {
final Result<Exam> updateSebClientRestriction = this.updateSebClientRestriction(exam);
if (updateSebClientRestriction.hasError()) {
log.error("Failed to update SEB Client restriction on LMS for exam: {}", exam);
}
}
// release the lock
this.examDAO.releaseLock(exam.id, updateId)
.getOrThrow();
return result;
})
.onError(TransactionHandler::rollback);
}
private void checkActiveClientConnections(final Exam exam) {
if (this.examSessionService.getConnectionData(exam.id)
.getOrThrow()
.stream()
.filter(ExamConfigUpdateServiceImpl::isActiveConnection)
.count() > 0) {
throw new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,
"Integrity violation: There are currently active SEB Client connection.");
}
}
@Override

View file

@ -124,7 +124,7 @@ public class ConfigurationController extends ReadonlyEntityController<Configurat
return Result.ofError(new NullPointerException("Configuration has null reference"));
}
return this.examConfigUpdateService.processSEBExamConfigurationChange(config.configurationNodeId)
return this.examConfigUpdateService.processExamConfigurationChange(config.configurationNodeId)
.map(ids -> {
log.info("Successfully updated SEB Configuration for exams: {}", ids);
return config;

View file

@ -316,12 +316,22 @@ public abstract class EntityController<T extends Entity, M extends Entity> {
return this.entityDAO.byModelId(modelId)
.flatMap(this::checkWriteAccess)
.flatMap(this::validForDelete)
.map(this::bulkDelete)
.flatMap(this::bulkDelete)
.flatMap(this::notifyDeleted)
.flatMap(pair -> this.logBulkAction(pair.b))
.getOrThrow();
}
protected Result<Pair<T, EntityProcessingReport>> bulkDelete(final T entity) {
return Result.tryCatch(() -> new Pair<>(
entity,
this.bulkActionService.createReport(new BulkAction(
BulkActionType.HARD_DELETE,
entity.entityType(),
new EntityName(entity.getModelId(), entity.entityType(), entity.getName())))
.getOrThrow()));
}
protected void checkReadPrivilege(final Long institutionId) {
this.authorization.check(
PrivilegeType.READ,
@ -500,14 +510,4 @@ public abstract class EntityController<T extends Entity, M extends Entity> {
* @return the MyBatis SqlTable for the concrete Entity */
protected abstract SqlTable getSQLTableOfEntity();
private Pair<T, EntityProcessingReport> bulkDelete(final T entity) {
return new Pair<>(
entity,
this.bulkActionService.createReport(new BulkAction(
BulkActionType.HARD_DELETE,
entity.entityType(),
new EntityName(entity.getModelId(), entity.entityType(), entity.getName())))
.getOrThrow());
}
}

View file

@ -9,8 +9,13 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
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.api.API;
@ -32,6 +37,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfigurationMapRecordDynamicSqlSupport;
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.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.EntityDAO;
@ -116,21 +122,53 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
}
@Override
protected Result<ExamConfigurationMap> validForSave(final ExamConfigurationMap entity) {
return super.validForSave(entity)
.map(this::checkPasswordMatch);
@RequestMapping(
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ExamConfigurationMap create(
@RequestParam final MultiValueMap<String, String> allRequestParams,
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
// check modify privilege for requested institution and concrete entityType
this.checkModifyPrivilege(institutionId);
final POSTMapper postMap = new POSTMapper(allRequestParams)
.putIfAbsent(API.PARAM_INSTITUTION_ID, String.valueOf(institutionId));
final ExamConfigurationMap requestModel = this.createNew(postMap);
return this.checkCreateAccess(requestModel)
.map(this::checkPasswordMatch)
.flatMap(entity -> this.examConfigUpdateService.processExamConfigurationMappingChange(
entity,
this.entityDAO::createNew))
.flatMap(this::logCreate)
.flatMap(this::notifyCreated)
.getOrThrow();
}
@Override
protected Result<ExamConfigurationMap> validForDelete(final ExamConfigurationMap entity) {
return super.validForDelete(entity)
.map(this::checkNoActiveClientConnections);
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT,
method = RequestMethod.DELETE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public EntityProcessingReport hardDelete(@PathVariable final String modelId) {
return this.entityDAO.byModelId(modelId)
.flatMap(this::checkWriteAccess)
.flatMap(entity -> this.examConfigUpdateService.processExamConfigurationMappingChange(
entity,
this::bulkDelete))
.flatMap(this::notifyDeleted)
.flatMap(pair -> this.logBulkAction(pair.b))
.getOrThrow();
}
@Override
protected Result<ExamConfigurationMap> notifyCreated(final ExamConfigurationMap entity) {
// update the attached configurations state to "In Use"
// and apply change to involved Exam
return this.configurationNodeDAO.save(new ConfigurationNode(
entity.configurationNodeId,
null,
@ -140,8 +178,6 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
null,
null,
ConfigurationStatus.IN_USE))
.flatMap(config -> this.examConfigUpdateService
.processSEBExamConfigurationAttachmentChange(entity.examId))
.map(id -> entity);
}
@ -149,9 +185,7 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
@Override
protected Result<Pair<ExamConfigurationMap, EntityProcessingReport>> notifyDeleted(
final Pair<ExamConfigurationMap, EntityProcessingReport> pair) {
// update the attached configurations state to "Ready"
// and apply change to involved Exam
return this.configurationNodeDAO.save(new ConfigurationNode(
pair.a.configurationNodeId,
null,
@ -161,8 +195,6 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
null,
null,
ConfigurationStatus.IN_USE))
.flatMap(config -> this.examConfigUpdateService
.processSEBExamConfigurationAttachmentChange(pair.a.examId))
.map(id -> pair);
}