Merge pull request #108 from SafeExamBrowser/SEBSERV-398

Sebserv 398
This commit is contained in:
Andreas Hefti 2024-02-08 08:27:47 +01:00 committed by GitHub
commit 8d1d9d8dda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 272 additions and 5 deletions

View file

@ -715,6 +715,12 @@ public enum ActionDefinition {
PageStateDefinitionImpl.SEB_EXAM_CONFIG_LIST,
ActionCategory.SEB_EXAM_CONFIG_LIST),
SEB_EXAM_CONFIG_BULK_DELETE(
new LocTextKey("sebserver.examconfig.list.batch.action.delete"),
ImageIcon.DELETE,
PageStateDefinitionImpl.SEB_EXAM_CONFIG_LIST,
ActionCategory.SEB_EXAM_CONFIG_LIST),
SEB_EXAM_CONFIG_MODIFY_PROP_FROM_LIST(
new LocTextKey("sebserver.examconfig.action.list.modify.properties"),
ImageIcon.EDIT,

View file

@ -0,0 +1,130 @@
package ch.ethz.seb.sebserver.gui.content.configs;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BatchActionType;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.BatchAction;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.AbstractBatchActionWizard;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNodesByIds;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import org.apache.tomcat.util.buf.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@Lazy
@Component
@GuiProfile
public class SEBExamConfigBatchDeletePopup extends AbstractBatchActionWizard {
private final static LocTextKey FORM_TITLE = new LocTextKey("sebserver.examconfig.list.batch.delete.title");
private final static LocTextKey FORM_INFO = new LocTextKey("sebserver.examconfig.list.batch.delete.info");
private final static LocTextKey ACTION_DO_DELETE = new LocTextKey("sebserver.examconfig.list.batch.action.delete.button");
protected SEBExamConfigBatchDeletePopup(
final PageService pageService,
final ServerPushService serverPushService) {
super(pageService, serverPushService);
}
@Override
protected LocTextKey getTitle() {
return FORM_TITLE;
}
@Override
protected LocTextKey getBatchActionInfo() {
return FORM_INFO;
}
@Override
protected LocTextKey getBatchActionTitle() {
return ACTION_DO_DELETE;
}
@Override
protected BatchActionType getBatchActionType() {
return BatchActionType.EXAM_CONFIG_DELETE;
}
@Override
protected Supplier<PageContext> createResultPageSupplier(PageContext pageContext, FormHandle<ConfigurationNode> formHandle) {
return () -> pageContext;
}
@Override
protected void extendBatchActionRequest(PageContext pageContext, RestCall<BatchAction>.RestCallBuilder batchActionRequestBuilder) {
// Nothing to do here
}
@Override
protected FormBuilder buildSpecificFormFields(PageContext formContext, FormBuilder formHead, boolean readonly) {
return formHead;
}
@Override
protected void processUpdateListAction(PageContext formContext) {
this.pageService.executePageAction(this.pageService.pageActionBuilder(formContext)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_LIST)
.create());
}
@Override
protected void applySelectionList(
final PageContext formContext,
final Set<EntityKey> multiSelection) {
final ResourceService resourceService = this.pageService.getResourceService();
final String ids = StringUtils.join(
multiSelection.stream().map(EntityKey::getModelId).collect(Collectors.toList()),
Constants.LIST_SEPARATOR_CHAR);
final RestService restService = this.pageService.getRestService();
final List<ConfigurationNode> selected = new ArrayList<>(restService.getBuilder(GetExamConfigNodesByIds.class)
.withQueryParam(API.PARAM_MODEL_ID_LIST, ids)
.call()
.getOr(Collections.emptyList()));
selected.sort((examConfig1, examConfig2) -> examConfig1.name.compareTo(examConfig2.name));
this.pageService.staticListTableBuilder(selected, EntityType.CONFIGURATION_NODE)
.withPaging(10)
.withColumn(new ColumnDefinition<>(
Domain.CONFIGURATION_NODE.ATTR_NAME,
SEBExamConfigList.NAME_TEXT_KEY,
ConfigurationNode::getName))
.withColumn(new ColumnDefinition<>(
Domain.CONFIGURATION_NODE.ATTR_STATUS,
SEBExamConfigList.STATUS_TEXT_KEY,
resourceService::localizedExamConfigStatusName)
)
.compose(formContext);
}
}

View file

@ -53,11 +53,11 @@ public class SEBExamConfigList implements TemplateComposer {
new LocTextKey("sebserver.examconfig.list.title");
private static final LocTextKey INSTITUTION_TEXT_KEY =
new LocTextKey("sebserver.examconfig.list.column.institution");
private static final LocTextKey NAME_TEXT_KEY =
public static final LocTextKey NAME_TEXT_KEY =
new LocTextKey("sebserver.examconfig.list.column.name");
private static final LocTextKey DESCRIPTION_TEXT_KEY =
new LocTextKey("sebserver.examconfig.list.column.description");
private static final LocTextKey STATUS_TEXT_KEY =
public static final LocTextKey STATUS_TEXT_KEY =
new LocTextKey("sebserver.examconfig.list.column.status");
private static final LocTextKey TEMPLATE_TEXT_KEY =
new LocTextKey("sebserver.examconfig.list.column.template");
@ -81,6 +81,8 @@ public class SEBExamConfigList implements TemplateComposer {
private final SEBExamConfigCreationPopup sebExamConfigCreationPopup;
private final SEBExamConfigBatchStateChangePopup sebExamConfigBatchStateChangePopup;
private final SEBExamConfigBatchResetToTemplatePopup sebExamConfigBatchResetToTemplatePopup;
private final SEBExamConfigBatchDeletePopup sebExamConfigBatchDeletePopup;
private final CurrentUser currentUser;
private final ResourceService resourceService;
private final int pageSize;
@ -91,6 +93,7 @@ public class SEBExamConfigList implements TemplateComposer {
final SEBExamConfigCreationPopup sebExamConfigCreationPopup,
final SEBExamConfigBatchStateChangePopup sebExamConfigBatchStateChangePopup,
final SEBExamConfigBatchResetToTemplatePopup sebExamConfigBatchResetToTemplatePopup,
final SEBExamConfigBatchDeletePopup sebExamConfigBatchDeletePopup,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
@ -98,6 +101,7 @@ public class SEBExamConfigList implements TemplateComposer {
this.sebExamConfigCreationPopup = sebExamConfigCreationPopup;
this.sebExamConfigBatchStateChangePopup = sebExamConfigBatchStateChangePopup;
this.sebExamConfigBatchResetToTemplatePopup = sebExamConfigBatchResetToTemplatePopup;
this.sebExamConfigBatchDeletePopup = sebExamConfigBatchDeletePopup;
this.currentUser = pageService.getCurrentUser();
this.resourceService = pageService.getResourceService();
this.pageSize = pageSize;
@ -188,7 +192,8 @@ public class SEBExamConfigList implements TemplateComposer {
ActionDefinition.SEB_EXAM_CONFIG_MODIFY_PROP_FROM_LIST,
ActionDefinition.SEB_EXAM_CONFIG_COPY_CONFIG_FROM_LIST,
ActionDefinition.SEB_EXAM_CONFIG_BULK_STATE_CHANGE,
ActionDefinition.SEB_EXAM_CONFIG_BULK_RESET_TO_TEMPLATE))
ActionDefinition.SEB_EXAM_CONFIG_BULK_RESET_TO_TEMPLATE,
ActionDefinition.SEB_EXAM_CONFIG_BULK_DELETE))
.compose(pageContext.copyOf(content));
@ -248,6 +253,14 @@ public class SEBExamConfigList implements TemplateComposer {
.noEventPropagation()
.publishIf(() -> !isLight && examConfigGrant.im(), false)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_BULK_DELETE)
.withSelect(
configTable::getMultiSelection,
this.sebExamConfigBatchDeletePopup.popupCreationFunction(pageContext),
EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(() -> !isLight && examConfigGrant.im(), false)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG)
.withSelect(
configTable.getGrantedSelection(this.currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),

View file

@ -0,0 +1,32 @@
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import java.util.Collection;
@Lazy
@Component
@GuiProfile
public class GetExamConfigNodesByIds extends RestCall<Collection<ConfigurationNode>> {
public GetExamConfigNodesByIds() {
super(new TypeKey<>(
CallType.GET_LIST,
EntityType.CONFIGURATION_NODE,
new TypeReference<Collection<ConfigurationNode>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.CONFIGURATION_NODE_ENDPOINT + API.LIST_PATH_SEGMENT
);
}
}

View file

@ -0,0 +1,82 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl;
import ch.ethz.seb.sebserver.gbl.api.API.BatchActionType;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.BatchAction;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BatchActionExec;
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.sebconfig.ExamConfigService;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.stream.Collectors;
@Lazy
@Component
@WebServiceProfile
public class ExamConfigDelete implements BatchActionExec {
private final ConfigurationNodeDAO configurationNodeDAO;
private final AuthorizationService authorizationService;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
public ExamConfigDelete(
final ExamConfigService sebExamConfigService,
final ConfigurationNodeDAO configurationNodeDAO,
final AuthorizationService authorizationService,
final ExamConfigurationMapDAO examConfigurationMapDAO) {
this.configurationNodeDAO = configurationNodeDAO;
this.authorizationService = authorizationService;
this.examConfigurationMapDAO = examConfigurationMapDAO;
}
@Override
public BatchActionType actionType() {
return BatchActionType.EXAM_CONFIG_DELETE;
}
@Override
public APIMessage checkConsistency(final Map<String, String> actionAttributes) {
// no additional check here
return null;
}
@Override
public Result<EntityKey> doSingleAction(final String modelId, final BatchAction batchAction) {
return this.configurationNodeDAO
.byModelId(modelId)
.flatMap(examConfig -> this.authorizationService.check(PrivilegeType.MODIFY, examConfig))
.flatMap(examConfig -> checkDeletionRequirements(examConfig))
.flatMap(examConfig -> this.configurationNodeDAO.delete(new HashSet<>(Arrays.asList(new EntityKey(
modelId,
EntityType.CONFIGURATION_NODE))))
)
.map(res -> res.stream().collect(Collectors.toList()).get(0));
}
private Result<ConfigurationNode> checkDeletionRequirements(ConfigurationNode examConfig){
Result<Boolean> isNotActive = this.examConfigurationMapDAO.checkNoActiveExamReferences(examConfig.id);
if(!isNotActive.getOrThrow()){
return Result.ofError(new APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam Configuration has active Exam references")
));
}
return Result.of(examConfig);
}
}

View file

@ -265,7 +265,7 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO {
@Override
@Transactional
public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) {
return Result.tryCatch(() -> {
return Result.<Collection<EntityKey>>tryCatch(() -> {
final List<Long> ids = extractListOfPKs(all);
@ -306,7 +306,7 @@ public class ConfigurationNodeDAOImpl implements ConfigurationNodeDAO {
return ids.stream()
.map(id -> new EntityKey(id, EntityType.CONFIGURATION_NODE))
.collect(Collectors.toList());
});
}).onError(TransactionHandler::rollback);
}
private void handleConfigTemplateDeletion(final List<Long> configurationIds) {

View file

@ -1097,6 +1097,10 @@ sebserver.examconfig.list.batch.action.status=Target Status
sebserver.examconfig.list.action.reset=Reset To Template Settings
sebserver.examconfig.list.batch.reset.title=Reset To Template Settings
sebserver.examconfig.list.batch.action.reset=Reset All
sebserver.examconfig.list.batch.action.delete=Delete Exam Configurations
sebserver.examconfig.list.batch.action.delete.button=Delete
sebserver.examconfig.list.batch.delete.title=Delete Selected Exam Configurations
sebserver.examconfig.list.batch.delete.info=Please Note:<br/>&nbsp;&nbsp;&nbsp;&nbsp;This deletes the exam configuration in SEB Server<br/>&nbsp;&nbsp;&nbsp;&nbsp;
sebserver.examconfig.list.batch.action.reset.info=This action process a reset of the SEB settings to its template settings for all selected configurations.<br/>Please note this will be done only for those that has an origin template and are not in state "Used". <br/>Please note also that this action tries to directly publish the changes and if not possible (active SEB client connections) it will report an error even if the settings has been reseted.
sebserver.examconfig.action.list.modify.properties=Edit Exam Configuration