diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index 1db8948e..2922a097 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -548,6 +548,11 @@ public enum ActionDefinition { PageStateDefinitionImpl.SEB_EXAM_CONFIG_VIEW, 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( new LocTextKey("sebserver.overall.action.modify.cancel"), ImageIcon.CANCEL, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBExamConfigForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBExamConfigForm.java index b8546f4e..6186cc93 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBExamConfigForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBExamConfigForm.java @@ -21,9 +21,12 @@ 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.APIMessage.ErrorMessage; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.Domain; 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.QuizData; 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.user.UserInfo; 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.gui.content.action.ActionDefinition; 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.impl.ModalInputDialog; 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.exam.GetExamConfigMappingNames; 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.GetExamConfigNode; 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"); static final LocTextKey FORM_IMPORT_ERROR_FILE_SELECTION = 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 RestService restService; @@ -217,9 +233,14 @@ public class SEBExamConfigForm implements TemplateComposer { .newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_MODIFY) .withEntityKey(entityKey) - .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) ? ActionDefinition.SEB_EXAM_CONFIG_VIEW : 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 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 table) { return this.pageService.pageActionBuilder(table.getPageContext()) .newAction(ActionDefinition.EXAM_VIEW_FROM_LIST) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamDeletePopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamDeletePopup.java index 4a403ac6..5ec0af5d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamDeletePopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamDeletePopup.java @@ -123,7 +123,9 @@ public class ExamDeletePopup { try { 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) .call() .getOrThrow(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/DeleteExamConfiguration.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/DeleteExamConfiguration.java new file mode 100644 index 00000000..ac22f347 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/seb/examconfig/DeleteExamConfiguration.java @@ -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 { + + public DeleteExamConfiguration() { + super(new TypeKey<>( + CallType.DELETE, + EntityType.CONFIGURATION_NODE, + new TypeReference() { + }), + HttpMethod.DELETE, + MediaType.APPLICATION_FORM_URLENCODED, + API.CONFIGURATION_NODE_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java index 3247002a..5e1fd011 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamConfigurationMapDAO.java @@ -50,7 +50,7 @@ public interface ExamConfigurationMapDAO extends Result getUserConfigurationNodeId(final Long examId, final String userId); /** Get a list of all ConfigurationNode identifiers of configurations that currently are attached to a given Exam - * + * * @param examId the Exam identifier * @return Result refers to a list of ConfigurationNode identifiers or refer to an error if happened */ Result> 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 */ Result> getExamIdsForConfigId(Long configurationId); + Result checkForDeletion(Long configurationNodeId); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java index 75554471..c4e60691 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java @@ -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.EntityKey; 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.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; @@ -372,6 +373,34 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { .flatMap(this::getExamIdsForConfigNodeId); } + @Override + @Transactional(readOnly = true) + public Result 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 recordById(final Long id) { return Result.tryCatch(() -> { final ExamConfigurationMapRecord record = this.examConfigurationMapRecordMapper diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java index 1ef3fe79..44d0185c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java @@ -37,6 +37,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; 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.POSTMapper; 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.dao.ConfigurationDAO; 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.OrientationDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; @@ -80,6 +83,7 @@ public class ConfigurationNodeController extends EntityController 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 protected Result notifyCreated(final ConfigurationNode entity) { return super.notifyCreated(entity) diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 54a35747..f88f3053 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -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.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.action.list.new=Add Exam Configuration sebserver.examconfig.action.list.view=View 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.view=View SEB Settings 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.state-change.confirm=This configuration is already attached to an exam.
Please note that changing an attached configuration will take effect on the exam when the configuration changes are saved

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.confirm.delete=This will completely delete the exam configuration.

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.
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:

{1} + sebserver.examconfig.form.title.new=Add Exam Configuration sebserver.examconfig.form.title=Exam Configuration