SEBSERV-260 Delete exam config

This commit is contained in:
anhefti 2022-01-25 14:40:16 +01:00
parent ecc5398147
commit 34fe5ba43f
8 changed files with 172 additions and 4 deletions

View file

@ -548,6 +548,11 @@ public enum ActionDefinition {
PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW, PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW,
ActionCategory.FORM), ActionCategory.FORM),
SEB_EXAM_CONFIG_DELETE(
new LocTextKey("sebserver.examconfig.action.delete"),
ImageIcon.DELETE,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_PROP_VIEW,
ActionCategory.FORM),
SEB_EXAM_CONFIG_PROP_CANCEL_MODIFY( SEB_EXAM_CONFIG_PROP_CANCEL_MODIFY(
new LocTextKey("sebserver.overall.action.modify.cancel"), new LocTextKey("sebserver.overall.action.modify.cancel"),
ImageIcon.CANCEL, ImageIcon.CANCEL,

View file

@ -21,9 +21,12 @@ 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.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
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.sebconfig.ConfigKey; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigKey;
@ -32,6 +35,7 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.Configuration
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationType;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Tuple; import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.content.exam.ExamList; import ch.ethz.seb.sebserver.gui.content.exam.ExamList;
@ -46,9 +50,11 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
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.page.impl.ModalInputDialog; import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingsPage; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingsPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.DeleteExamConfiguration;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ExportConfigKey; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ExportConfigKey;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNode; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNode;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.NewExamConfig; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.NewExamConfig;
@ -106,6 +112,16 @@ public class SEBExamConfigForm implements TemplateComposer {
new LocTextKey("sebserver.error.unexpected"); new LocTextKey("sebserver.error.unexpected");
static final LocTextKey FORM_IMPORT_ERROR_FILE_SELECTION = static final LocTextKey FORM_IMPORT_ERROR_FILE_SELECTION =
new LocTextKey("sebserver.examconfig.message.error.file"); new LocTextKey("sebserver.examconfig.message.error.file");
static final LocTextKey CONFIRM_DELETE =
new LocTextKey("sebserver.examconfig.message.confirm.delete");
static final LocTextKey DELETE_CONFIRM_TITLE =
new LocTextKey("sebserver.dialog.confirm.title");
private final static LocTextKey DELETE_ERROR_CONSISTENCY =
new LocTextKey("sebserver.examconfig.message.consistency.error");
private final static LocTextKey DELETE_ERROR_DEPENDENCY =
new LocTextKey("sebserver.examconfig.message.delete.partialerror");
private final static LocTextKey DELETE_CONFIRM =
new LocTextKey("sebserver.examconfig.message.delete.confirm");
private final PageService pageService; private final PageService pageService;
private final RestService restService; private final RestService restService;
@ -217,9 +233,14 @@ public class SEBExamConfigForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_MODIFY) .newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_MODIFY)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.publishIf(() -> modifyGrant && isReadonly) .publishIf(() -> modifyGrant && isReadonly)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_DELETE)
.withEntityKey(entityKey)
.withConfirm(() -> CONFIRM_DELETE)
.withExec(this::deleteConfiguration)
.publishIf(() -> writeGrant && examConfig.status != ConfigurationStatus.IN_USE && isReadonly)
.newAction((!modifyGrant || examConfig.status == ConfigurationStatus.IN_USE) .newAction((!modifyGrant || examConfig.status == ConfigurationStatus.IN_USE)
? ActionDefinition.SEB_EXAM_CONFIG_VIEW ? ActionDefinition.SEB_EXAM_CONFIG_VIEW
: ActionDefinition.SEB_EXAM_CONFIG_MODIFY) : ActionDefinition.SEB_EXAM_CONFIG_MODIFY)
@ -320,6 +341,51 @@ public class SEBExamConfigForm implements TemplateComposer {
} }
} }
private PageAction deleteConfiguration(final PageAction action) {
final ConfigurationNode configNode = this.restService
.getBuilder(GetExamConfigNode.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
.call()
.getOrThrow();
final Result<EntityProcessingReport> call = this.restService
.getBuilder(DeleteExamConfiguration.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
.call();
final PageContext pageContext = action.pageContext();
if (call.hasError()) {
final Exception error = call.getError();
if (error instanceof RestCallError) {
final APIMessage message = ((RestCallError) error)
.getAPIMessages()
.stream()
.findFirst()
.orElse(null);
if (message != null && ErrorMessage.INTEGRITY_VALIDATION.isOf(message)) {
pageContext.publishPageMessage(new PageMessageException(DELETE_ERROR_CONSISTENCY));
return action;
}
}
}
final EntityProcessingReport report = call.getOrThrow();
final String configName = configNode.toName().name;
if (report.getErrors().isEmpty()) {
pageContext.publishPageMessage(DELETE_CONFIRM_TITLE, new LocTextKey(DELETE_CONFIRM.name, configName));
} else {
pageContext.publishPageMessage(
DELETE_CONFIRM_TITLE,
new LocTextKey(DELETE_ERROR_DEPENDENCY.name, configName,
report.getErrors().iterator().next().getErrorMessage().systemMessage));
}
return this.pageService.pageActionBuilder(pageContext)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_LIST)
.create();
}
private PageAction showExamAction(final EntityTable<ExamConfigurationMap> table) { private PageAction showExamAction(final EntityTable<ExamConfigurationMap> table) {
return this.pageService.pageActionBuilder(table.getPageContext()) return this.pageService.pageActionBuilder(table.getPageContext())
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST) .newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)

View file

@ -123,7 +123,9 @@ public class ExamDeletePopup {
try { try {
final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey entityKey = pageContext.getEntityKey();
final Exam examToDelete = this.pageService.getRestService().getBuilder(GetExam.class) final Exam examToDelete = this.pageService
.getRestService()
.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call() .call()
.getOrThrow(); .getOrThrow();

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class DeleteExamConfiguration extends RestCall<EntityProcessingReport> {
public DeleteExamConfiguration() {
super(new TypeKey<>(
CallType.DELETE,
EntityType.CONFIGURATION_NODE,
new TypeReference<EntityProcessingReport>() {
}),
HttpMethod.DELETE,
MediaType.APPLICATION_FORM_URLENCODED,
API.CONFIGURATION_NODE_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT);
}
}

View file

@ -50,7 +50,7 @@ public interface ExamConfigurationMapDAO extends
Result<Long> getUserConfigurationNodeId(final Long examId, final String userId); Result<Long> getUserConfigurationNodeId(final Long examId, final String userId);
/** Get a list of all ConfigurationNode identifiers of configurations that currently are attached to a given Exam /** Get a list of all ConfigurationNode identifiers of configurations that currently are attached to a given Exam
* *
* @param examId the Exam identifier * @param examId the Exam identifier
* @return Result refers to a list of ConfigurationNode identifiers or refer to an error if happened */ * @return Result refers to a list of ConfigurationNode identifiers or refer to an error if happened */
Result<Collection<Long>> getConfigurationNodeIds(Long examId); Result<Collection<Long>> getConfigurationNodeIds(Long examId);
@ -67,4 +67,6 @@ public interface ExamConfigurationMapDAO extends
* @return Result referencing the List of exam identifiers (PK) for a given configuration identifier */ * @return Result referencing the List of exam identifiers (PK) for a given configuration identifier */
Result<Collection<Long>> getExamIdsForConfigId(Long configurationId); Result<Collection<Long>> getExamIdsForConfigId(Long configurationId);
Result<Boolean> checkForDeletion(Long configurationNodeId);
} }

View file

@ -34,6 +34,7 @@ import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService;
import ch.ethz.seb.sebserver.gbl.model.EntityDependency; import ch.ethz.seb.sebserver.gbl.model.EntityDependency;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
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.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus;
@ -372,6 +373,34 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
.flatMap(this::getExamIdsForConfigNodeId); .flatMap(this::getExamIdsForConfigNodeId);
} }
@Override
@Transactional(readOnly = true)
public Result<Boolean> checkForDeletion(final Long configurationNodeId) {
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper.selectByExample()
.where(
ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId,
isEqualTo(configurationNodeId))
.build()
.execute()
.stream()
.filter(rec -> !isExamFinished(rec.getExamId()))
.findFirst()
.isEmpty());
}
private boolean isExamFinished(final Long examId) {
try {
return this.examRecordMapper.countByExample()
.where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId))
.and(ExamRecordDynamicSqlSupport.status, isEqualTo(ExamStatus.FINISHED.name()))
.build()
.execute() >= 1;
} catch (final Exception e) {
log.warn("Failed to check exam status for exam: {}", examId, e);
return false;
}
}
private Result<ExamConfigurationMapRecord> recordById(final Long id) { private Result<ExamConfigurationMapRecord> recordById(final Long id) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final ExamConfigurationMapRecord record = this.examConfigurationMapRecordMapper final ExamConfigurationMapRecord record = this.examConfigurationMapRecordMapper

View file

@ -37,6 +37,8 @@ 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.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.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
@ -63,6 +65,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServe
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.ConfigurationNodeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.OrientationDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.OrientationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
@ -80,6 +83,7 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
private final ConfigurationNodeDAO configurationNodeDAO; private final ConfigurationNodeDAO configurationNodeDAO;
private final ConfigurationDAO configurationDAO; private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ViewDAO viewDAO; private final ViewDAO viewDAO;
private final OrientationDAO orientationDAO; private final OrientationDAO orientationDAO;
private final ExamConfigService sebExamConfigService; private final ExamConfigService sebExamConfigService;
@ -90,6 +94,7 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
final BulkActionService bulkActionService, final BulkActionService bulkActionService,
final ConfigurationNodeDAO entityDAO, final ConfigurationNodeDAO entityDAO,
final UserActivityLogDAO userActivityLogDAO, final UserActivityLogDAO userActivityLogDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final PaginationService paginationService, final PaginationService paginationService,
final BeanValidationService beanValidationService, final BeanValidationService beanValidationService,
final ConfigurationDAO configurationDAO, final ConfigurationDAO configurationDAO,
@ -107,6 +112,7 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
this.configurationDAO = configurationDAO; this.configurationDAO = configurationDAO;
this.configurationNodeDAO = entityDAO; this.configurationNodeDAO = entityDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.viewDAO = viewDAO; this.viewDAO = viewDAO;
this.orientationDAO = orientationDAO; this.orientationDAO = orientationDAO;
this.sebExamConfigService = sebExamConfigService; this.sebExamConfigService = sebExamConfigService;
@ -508,6 +514,19 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
}); });
} }
@Override
protected Result<ConfigurationNode> validForDelete(final ConfigurationNode entity) {
return Result.tryCatch(() -> {
if (!this.examConfigurationMapDAO.checkForDeletion(entity.id).getOr(false)) {
throw new APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam configuration has references to at least one upcoming or running exam."));
}
return entity;
});
}
@Override @Override
protected Result<ConfigurationNode> notifyCreated(final ConfigurationNode entity) { protected Result<ConfigurationNode> notifyCreated(final ConfigurationNode entity) {
return super.notifyCreated(entity) return super.notifyCreated(entity)

View file

@ -805,11 +805,11 @@ sebserver.examconfig.list.actions=
sebserver.examconfig.list.empty=There is currently no Exam configuration available. Please create a new one sebserver.examconfig.list.empty=There is currently no Exam configuration available. Please create a new one
sebserver.examconfig.info.pleaseSelect=At first please select an Exam Configuration from the list sebserver.examconfig.info.pleaseSelect=At first please select an Exam Configuration from the list
sebserver.examconfig.list.action.no.modify.privilege=No Access: An Exam Configuration from other institution cannot be modified. sebserver.examconfig.list.action.no.modify.privilege=No Access: An Exam Configuration from other institution cannot be modified.
sebserver.examconfig.action.list.new=Add Exam Configuration sebserver.examconfig.action.list.new=Add Exam Configuration
sebserver.examconfig.action.list.view=View Exam Configuration sebserver.examconfig.action.list.view=View Exam Configuration
sebserver.examconfig.action.list.modify.properties=Edit Exam Configuration sebserver.examconfig.action.list.modify.properties=Edit Exam Configuration
sebserver.examconfig.action.delete=Delete Exam Configuration
sebserver.examconfig.action.modify=Edit SEB Settings sebserver.examconfig.action.modify=Edit SEB Settings
sebserver.examconfig.action.view=View SEB Settings sebserver.examconfig.action.view=View SEB Settings
sebserver.examconfig.action.modify.properties=Edit Exam Configuration sebserver.examconfig.action.modify.properties=Edit Exam Configuration
@ -838,6 +838,11 @@ sebserver.examconfig.action.import.auto-publish=Publish
sebserver.examconfig.action.import.auto-publish.tooltip=Try to automatically publish the imported changes sebserver.examconfig.action.import.auto-publish.tooltip=Try to automatically publish the imported changes
sebserver.examconfig.action.state-change.confirm=This configuration is already attached to an exam.<br/>Please note that changing an attached configuration will take effect on the exam when the configuration changes are saved<br/><br/>Are you sure to change this configuration to an editable state? sebserver.examconfig.action.state-change.confirm=This configuration is already attached to an exam.<br/>Please note that changing an attached configuration will take effect on the exam when the configuration changes are saved<br/><br/>Are you sure to change this configuration to an editable state?
sebserver.examconfig.message.error.file=Please select a valid SEB Exam Configuration File sebserver.examconfig.message.error.file=Please select a valid SEB Exam Configuration File
sebserver.examconfig.message.confirm.delete=This will completely delete the exam configuration.<br/><br/>Are you sure you want to delete this exam configuration?
sebserver.examconfig.message.consistency.error=The exam configuration cannot be deleted since it is used by at least one running or upcoming exam.<br/>Please remove the exam configuration from running and upcoming exams first.
sebserver.examconfig.message.delete.confirm=The exam configuration ({0}) was successfully deleted.
sebserver.examconfig.message.delete.partialerror=The exam configuration ({0}) was deleted but there where some dependency errors:<br/><br/>{1}
sebserver.examconfig.form.title.new=Add Exam Configuration sebserver.examconfig.form.title.new=Add Exam Configuration
sebserver.examconfig.form.title=Exam Configuration sebserver.examconfig.form.title=Exam Configuration