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,
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,

View file

@ -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<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) {
return this.pageService.pageActionBuilder(table.getPageContext())
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)

View file

@ -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();

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);
/** 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<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 */
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.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<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) {
return Result.tryCatch(() -> {
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 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<ConfigurationN
private final ConfigurationNodeDAO configurationNodeDAO;
private final ConfigurationDAO configurationDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final ViewDAO viewDAO;
private final OrientationDAO orientationDAO;
private final ExamConfigService sebExamConfigService;
@ -90,6 +94,7 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
final BulkActionService bulkActionService,
final ConfigurationNodeDAO entityDAO,
final UserActivityLogDAO userActivityLogDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ConfigurationDAO configurationDAO,
@ -107,6 +112,7 @@ public class ConfigurationNodeController extends EntityController<ConfigurationN
this.configurationDAO = configurationDAO;
this.configurationNodeDAO = entityDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.viewDAO = viewDAO;
this.orientationDAO = orientationDAO;
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
protected Result<ConfigurationNode> notifyCreated(final ConfigurationNode 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.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.<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.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=Exam Configuration