SEBSERV-73 seb exam config add/remove on running exam
This commit is contained in:
parent
d1f80baa87
commit
810b6dc8c2
5 changed files with 130 additions and 37 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue