From f380cd49f91434d7b0fb33ecdd7a6bf2cff3cea3 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 23 Feb 2023 15:23:38 +0100 Subject: [PATCH] SEBSERV-353 implementation --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 4 +- .../gbl/model/user/UserLogActivityType.java | 3 +- .../gui/content/action/ActionDefinition.java | 11 +- ...EBExamConfigBatchResetToTemplatePopup.java | 3 - .../content/exam/ExamBatchArchivePopup.java | 146 ++++++++++++++++++ .../content/exam/ExamBatchDeletePopup.java | 144 +++++++++++++++++ .../sebserver/gui/content/exam/ExamList.java | 32 +++- .../page/AbstractBatchActionWizard.java | 8 + .../webservice/api/exam/GetExamsByIds.java | 43 ++++++ .../gui/table/StaticListPageSupplier.java | 6 +- .../bulkaction/impl/ArchiveExamAction.java | 86 +++++++++++ .../bulkaction/impl/DeleteExamAction.java | 112 ++++++++++++++ .../impl/ExamConfigResetToTemplate.java | 1 - .../servicelayer/dao/UserActivityLogDAO.java | 19 +++ .../dao/impl/UserActivityLogDAOImpl.java | 44 +++++- .../api/ExamAdministrationController.java | 2 +- src/main/resources/messages.properties | 10 ++ 17 files changed, 656 insertions(+), 18 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamBatchArchivePopup.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamBatchDeletePopup.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetExamsByIds.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/ArchiveExamAction.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 54ea1f11..bc74ef3c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -20,7 +20,9 @@ public final class API { public enum BatchActionType { EXAM_CONFIG_STATE_CHANGE(EntityType.CONFIGURATION_NODE), - EXAM_CONFIG_REST_TEMPLATE_SETTINGS(EntityType.CONFIGURATION_NODE); + EXAM_CONFIG_REST_TEMPLATE_SETTINGS(EntityType.CONFIGURATION_NODE), + ARCHIVE_EXAM(EntityType.EXAM), + DELETE_EXAM(EntityType.EXAM); public final EntityType entityType; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserLogActivityType.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserLogActivityType.java index 1ad5c6d6..ffbb11ef 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserLogActivityType.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserLogActivityType.java @@ -21,5 +21,6 @@ public enum UserLogActivityType { FINISHED, DELETE, LOGIN, - LOGOUT + LOGOUT, + ARCHIVE } \ No newline at end of file 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 1ab0e433..bf8d52d4 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 @@ -307,6 +307,16 @@ public enum ActionDefinition { ImageIcon.TOGGLE_ON, PageStateDefinitionImpl.EXAM_LIST, ActionCategory.EXAM_LIST), + EXAM_LIST_BULK_ARCHIVE( + new LocTextKey("sebserver.exam.list.action.archive"), + ImageIcon.ARCHIVE, + PageStateDefinitionImpl.EXAM_LIST, + ActionCategory.EXAM_LIST), + EXAM_LIST_BULK_DELETE( + new LocTextKey("sebserver.exam.list.action.delete"), + ImageIcon.DELETE, + PageStateDefinitionImpl.EXAM_LIST, + ActionCategory.EXAM_LIST), EXAM_MODIFY_SEB_RESTRICTION_DETAILS( new LocTextKey("sebserver.exam.action.sebrestriction.details"), @@ -683,7 +693,6 @@ public enum ActionDefinition { ImageIcon.SWITCH, PageStateDefinitionImpl.SEB_EXAM_CONFIG_LIST, ActionCategory.SEB_EXAM_CONFIG_LIST), - SEB_EXAM_CONFIG_BULK_RESET_TO_TEMPLATE( new LocTextKey("sebserver.examconfig.list.action.reset"), ImageIcon.EXPORT, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBExamConfigBatchResetToTemplatePopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBExamConfigBatchResetToTemplatePopup.java index b6158cfe..f424c0aa 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBExamConfigBatchResetToTemplatePopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/configs/SEBExamConfigBatchResetToTemplatePopup.java @@ -69,7 +69,6 @@ public class SEBExamConfigBatchResetToTemplatePopup extends AbstractBatchActionW protected Supplier createResultPageSupplier( final PageContext pageContext, final FormHandle formHandle) { - // No specific fields for this action return () -> pageContext; } @@ -78,7 +77,6 @@ public class SEBExamConfigBatchResetToTemplatePopup extends AbstractBatchActionW protected void extendBatchActionRequest( final PageContext pageContext, final RestCall.RestCallBuilder batchActionRequestBuilder) { - // Nothing to do here } @@ -87,7 +85,6 @@ public class SEBExamConfigBatchResetToTemplatePopup extends AbstractBatchActionW final PageContext formContext, final FormBuilder formHead, final boolean readonly) { - // No specific fields for this action return formHead; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamBatchArchivePopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamBatchArchivePopup.java new file mode 100644 index 00000000..8bf5343d --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamBatchArchivePopup.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 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.content.exam; + +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; + +import org.apache.tomcat.util.buf.StringUtils; +import org.springframework.context.annotation.Lazy; +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.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.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +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.exam.GetExamsByIds; +import ch.ethz.seb.sebserver.gui.table.ColumnDefinition; + +@Lazy +@Component +@GuiProfile +public class ExamBatchArchivePopup extends AbstractBatchActionWizard { + + private final static LocTextKey FORM_TITLE = + new LocTextKey("sebserver.exam.list.batch.archive.title"); + private final static LocTextKey ACTION_DO_RESET = + new LocTextKey("sebserver.exam.list.batch.action.archive"); + private final static LocTextKey FORM_INFO = + new LocTextKey("sebserver.exam.list.batch.action.archive.info"); + + protected ExamBatchArchivePopup(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_RESET; + } + + @Override + protected BatchActionType getBatchActionType() { + return BatchActionType.ARCHIVE_EXAM; + } + + @Override + protected Supplier createResultPageSupplier( + final PageContext pageContext, + final FormHandle formHandle) { + + // No specific fields for this action + return () -> pageContext; + } + + @Override + protected void extendBatchActionRequest( + final PageContext pageContext, + final RestCall.RestCallBuilder batchActionRequestBuilder) { + // Nothing to do here + } + + @Override + protected FormBuilder buildSpecificFormFields( + final PageContext formContext, + final FormBuilder formHead, + final boolean readonly) { + + // No specific fields for this action + return formHead; + } + + @Override + protected void applySelectionList( + final PageContext formContext, + final Set 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 selected = new ArrayList<>(restService.getBuilder(GetExamsByIds.class) + .withQueryParam(API.PARAM_MODEL_ID_LIST, ids) + .call() + .getOr(Collections.emptyList())); + + selected.sort((exam1, exam2) -> exam1.name.compareTo(exam2.name)); + + this.pageService.staticListTableBuilder(selected, EntityType.EXAM) + .withPaging(10) + .withColumn(new ColumnDefinition<>( + Domain.EXAM.ATTR_LMS_SETUP_ID, + ExamList.COLUMN_TITLE_LMS_KEY, + ExamList.examLmsSetupNameFunction(resourceService))) + + .withColumn(new ColumnDefinition<>( + Domain.EXAM.ATTR_QUIZ_NAME, + ExamList.COLUMN_TITLE_NAME_KEY, + Exam::getName)) + + .withColumn(new ColumnDefinition<>( + Domain.EXAM.ATTR_STATUS, + ExamList.COLUMN_TITLE_STATE_KEY, + resourceService::localizedExamStatusName)) + + .compose(formContext); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamBatchDeletePopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamBatchDeletePopup.java new file mode 100644 index 00000000..793ab850 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamBatchDeletePopup.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2023 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.content.exam; + +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; + +import org.apache.tomcat.util.buf.StringUtils; +import org.springframework.context.annotation.Lazy; +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.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.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +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.exam.GetExamsByIds; +import ch.ethz.seb.sebserver.gui.table.ColumnDefinition; + +@Lazy +@Component +@GuiProfile +public class ExamBatchDeletePopup extends AbstractBatchActionWizard { + + private final static LocTextKey FORM_TITLE = + new LocTextKey("sebserver.exam.list.batch.delete.title"); + private final static LocTextKey ACTION_DO_RESET = + new LocTextKey("sebserver.exam.list.batch.action.delete"); + private final static LocTextKey FORM_INFO = + new LocTextKey("sebserver.exam.list.batch.action.delete.info"); + + protected ExamBatchDeletePopup(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_RESET; + } + + @Override + protected BatchActionType getBatchActionType() { + return BatchActionType.DELETE_EXAM; + } + + @Override + protected Supplier createResultPageSupplier( + final PageContext pageContext, + final FormHandle formHandle) { + // No specific fields for this action + return () -> pageContext; + } + + @Override + protected void extendBatchActionRequest(final PageContext pageContext, + final RestCall.RestCallBuilder batchActionRequestBuilder) { + // Nothing to do here + } + + @Override + protected FormBuilder buildSpecificFormFields( + final PageContext formContext, + final FormBuilder formHead, + final boolean readonly) { + + // No specific fields for this action + return formHead; + } + + @Override + protected void applySelectionList( + final PageContext formContext, + final Set 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 selected = new ArrayList<>(restService.getBuilder(GetExamsByIds.class) + .withQueryParam(API.PARAM_MODEL_ID_LIST, ids) + .call() + .getOr(Collections.emptyList())); + + selected.sort((exam1, exam2) -> exam1.name.compareTo(exam2.name)); + + this.pageService.staticListTableBuilder(selected, EntityType.EXAM) + .withPaging(10) + .withColumn(new ColumnDefinition<>( + Domain.EXAM.ATTR_LMS_SETUP_ID, + ExamList.COLUMN_TITLE_LMS_KEY, + ExamList.examLmsSetupNameFunction(resourceService))) + + .withColumn(new ColumnDefinition<>( + Domain.EXAM.ATTR_QUIZ_NAME, + ExamList.COLUMN_TITLE_NAME_KEY, + Exam::getName)) + + .withColumn(new ColumnDefinition<>( + Domain.EXAM.ATTR_STATUS, + ExamList.COLUMN_TITLE_STATE_KEY, + resourceService::localizedExamStatusName)) + + .compose(formContext); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamList.java index 6d0dc0ac..bc693d15 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamList.java @@ -98,12 +98,19 @@ public class ExamList implements TemplateComposer { private final ResourceService resourceService; private final int pageSize; + private final ExamBatchArchivePopup examBatchArchivePopup; + private final ExamBatchDeletePopup examBatchDeletePopup; + protected ExamList( final PageService pageService, + final ExamBatchArchivePopup examBatchArchivePopup, + final ExamBatchDeletePopup examBatchDeletePopup, @Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) { this.pageService = pageService; this.resourceService = pageService.getResourceService(); + this.examBatchArchivePopup = examBatchArchivePopup; + this.examBatchDeletePopup = examBatchDeletePopup; this.pageSize = pageSize; this.institutionFilter = new TableFilterAttribute( @@ -156,6 +163,7 @@ public class ExamList implements TemplateComposer { // table final EntityTable table = this.pageService.entityTableBuilder(restService.getRestCall(GetExamPage.class)) + .withMultiSelection() .withEmptyMessage(EMPTY_LIST_TEXT_KEY) .withPaging(this.pageSize) .withRowDecorator(decorateOnExamConsistency(this.pageService)) @@ -224,7 +232,9 @@ public class ExamList implements TemplateComposer { .withSelectionListener(this.pageService.getSelectionPublisher( pageContext, ActionDefinition.EXAM_VIEW_FROM_LIST, - ActionDefinition.EXAM_MODIFY_FROM_LIST)) + ActionDefinition.EXAM_MODIFY_FROM_LIST, + ActionDefinition.EXAM_LIST_BULK_ARCHIVE, + ActionDefinition.EXAM_LIST_BULK_DELETE)) .compose(pageContext.copyOf(content)); @@ -241,7 +251,23 @@ public class ExamList implements TemplateComposer { table.getGrantedSelection(currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION), action -> modifyExam(action, table), EMPTY_SELECTION_TEXT_KEY) - .publishIf(() -> userGrant.im(), false); + .publishIf(() -> userGrant.im(), false) + + .newAction(ActionDefinition.EXAM_LIST_BULK_ARCHIVE) + .withSelect( + table::getMultiSelection, + this.examBatchArchivePopup.popupCreationFunction(pageContext), + EMPTY_SELECTION_TEXT_KEY) + .noEventPropagation() + .publishIf(() -> userGrant.im(), false) + + .newAction(ActionDefinition.EXAM_LIST_BULK_DELETE) + .withSelect( + table::getMultiSelection, + this.examBatchDeletePopup.popupCreationFunction(pageContext), + EMPTY_SELECTION_TEXT_KEY) + .noEventPropagation() + .publishIf(() -> userGrant.iw(), false); actionBuilder .newAction(ActionDefinition.EXAM_LIST_HIDE_MISSING) @@ -322,7 +348,7 @@ public class ExamList implements TemplateComposer { }); } - private static Function examLmsSetupNameFunction(final ResourceService resourceService) { + public static Function examLmsSetupNameFunction(final ResourceService resourceService) { return exam -> resourceService.getLmsSetupNameFunction() .apply(String.valueOf(exam.lmsSetupId)); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/AbstractBatchActionWizard.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/AbstractBatchActionWizard.java index 253e1875..4e65d268 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/AbstractBatchActionWizard.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/AbstractBatchActionWizard.java @@ -84,6 +84,12 @@ public abstract class AbstractBatchActionWizard { final FormBuilder formHead, final boolean readonly); + protected void applySelectionList( + final PageContext formContext, + final Set multiSelection) { + + } + public Function popupCreationFunction(final PageContext pageContext) { return action -> { @@ -135,6 +141,8 @@ public abstract class AbstractBatchActionWizard { false) .build(); + applySelectionList(formContext, multiSelection); + return createResultPageSupplier(pageContext, formHandle); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetExamsByIds.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetExamsByIds.java new file mode 100644 index 00000000..917e00c0 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetExamsByIds.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 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.exam; + +import java.util.Collection; + +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.exam.Exam; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetExamsByIds extends RestCall> { + + public GetExamsByIds() { + super(new TypeKey<>( + CallType.GET_LIST, + EntityType.EXAM, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.LIST_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/StaticListPageSupplier.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/StaticListPageSupplier.java index b7ace63f..515fb406 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/StaticListPageSupplier.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/StaticListPageSupplier.java @@ -101,11 +101,13 @@ public class StaticListPageSupplier implements PageSupplier { return new Page<>(1, 1, this.column, this.list); } - final int numOfPages = this.list.size() / this.pageSize; - + int numOfPages = this.list.size() / this.pageSize; if (numOfPages <= 0) { return new Page<>(1, 1, this.column, this.list); } + if (this.list.size() % this.pageSize > 0) { + numOfPages++; + } int from = (this.pageNumber - 1) * this.pageSize; if (from < 0) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/ArchiveExamAction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/ArchiveExamAction.java new file mode 100644 index 00000000..287def55 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/ArchiveExamAction.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 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.webservice.servicelayer.bulkaction.impl; + +import java.util.Map; + +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.api.API.BatchActionType; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.model.BatchAction; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType; +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.ExamDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; + +@Lazy +@Component +@WebServiceProfile +public class ArchiveExamAction implements BatchActionExec { + + private final ExamDAO examDAO; + private final ExamAdminService examAdminService; + private final AuthorizationService authorization; + private final UserActivityLogDAO userActivityLogDAO; + + public ArchiveExamAction( + final ExamDAO examDAO, + final ExamAdminService examAdminService, + final AuthorizationService authorization, + final UserActivityLogDAO userActivityLogDAO) { + + this.examDAO = examDAO; + this.examAdminService = examAdminService; + this.authorization = authorization; + this.userActivityLogDAO = userActivityLogDAO; + } + + @Override + public BatchActionType actionType() { + return BatchActionType.ARCHIVE_EXAM; + } + + @Override + public APIMessage checkConsistency(final Map actionAttributes) { + // no additional check here + return null; + } + + @Override + public Result doSingleAction(final String modelId, final BatchAction batchAction) { + return this.examDAO.byModelId(modelId) + .flatMap(this::checkWriteAccess) + .flatMap(this.examAdminService::archiveExam) + .flatMap(exam -> logArchived(exam, batchAction)) + .map(Exam::getEntityKey); + } + + private Result checkWriteAccess(final Exam entity) { + if (entity != null) { + this.authorization.checkWrite(entity); + } + return Result.of(entity); + } + + private Result logArchived(final Exam entity, final BatchAction batchAction) { + return this.userActivityLogDAO.log( + batchAction.ownerId, + UserLogActivityType.ARCHIVE, + entity, + "Part of batch action: " + batchAction.processorId); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java new file mode 100644 index 00000000..8779bc40 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 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.webservice.servicelayer.bulkaction.impl; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; + +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.api.API.BatchActionType; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; +import ch.ethz.seb.sebserver.gbl.model.BatchAction; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType; +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.ExamDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; + +@Lazy +@Component +@WebServiceProfile +public class DeleteExamAction implements BatchActionExec { + + private final ExamDAO examDAO; + private final AuthorizationService authorization; + private final UserActivityLogDAO userActivityLogDAO; + private final ExamSessionService examSessionService; + + public DeleteExamAction( + final ExamDAO examDAO, + final AuthorizationService authorization, + final UserActivityLogDAO userActivityLogDAO, + final ExamSessionService examSessionService) { + + this.examDAO = examDAO; + this.authorization = authorization; + this.userActivityLogDAO = userActivityLogDAO; + this.examSessionService = examSessionService; + } + + @Override + public BatchActionType actionType() { + return BatchActionType.DELETE_EXAM; + } + + @Override + public APIMessage checkConsistency(final Map actionAttributes) { + // no additional check here + return null; + } + + @Override + public Result doSingleAction(final String modelId, final BatchAction batchAction) { + return this.examDAO.byModelId(modelId) + .flatMap(this::checkWriteAccess) + .flatMap(this::checkNoActiveSEBClientConnections) + .flatMap(this::deleteExamWithRefs) + .flatMap(exam -> logDeleted(exam, batchAction)) + .map(Exam::getEntityKey); + } + + private Result deleteExamWithRefs(final Exam entity) { + final Result> delete = + this.examDAO.delete(new HashSet<>(Arrays.asList(entity.getEntityKey()))); + if (delete.hasError()) { + return Result.ofError(delete.getError()); + } else { + return Result.of(entity); + } + } + + private Result checkWriteAccess(final Exam entity) { + if (entity != null) { + this.authorization.checkWrite(entity); + } + return Result.of(entity); + } + + private Result checkNoActiveSEBClientConnections(final Exam exam) { + if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) { + return Result.ofError(new APIMessageException( + APIMessage.ErrorMessage.INTEGRITY_VALIDATION + .of("Exam currently has active SEB Client connections."))); + } + + return Result.of(exam); + } + + private Result logDeleted(final Exam entity, final BatchAction batchAction) { + return this.userActivityLogDAO.log( + batchAction.ownerId, + UserLogActivityType.DELETE, + entity, + "Part of batch action: " + batchAction.processorId); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/ExamConfigResetToTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/ExamConfigResetToTemplate.java index cbbe1477..c7faef08 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/ExamConfigResetToTemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/ExamConfigResetToTemplate.java @@ -76,7 +76,6 @@ public class ExamConfigResetToTemplate implements BatchActionExec { return node; }) .map(Entity::getEntityKey); - } private ConfigurationNode checkConsistency(final ConfigurationNode configurationNode) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java index 4e2f0c99..6c7c4dc3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java @@ -78,6 +78,12 @@ public interface UserActivityLogDAO extends * @return Result of the Entity or referring to an Error if happened */ Result logModify(E entity); + /** Create a user activity log entry for the current user of activity type ARCHIVE + * + * @param entity the Entity + * @return Result of the Entity or referring to an Error if happened */ + Result logArchive(E entity); + /** Create a user activity log entry for the current user of activity type FINISHED * * @param entity the Entity @@ -146,6 +152,19 @@ public interface UserActivityLogDAO extends E entity, String message); + /** Creates a user activity log entry. + * + * @param userUUID The User UUID + * @param activityType the activity type + * @param entity the Entity + * @param message an optional message + * @return Result of the Entity or referring to an Error if happened */ + Result log( + String userUUID, + UserLogActivityType activityType, + E entity, + String message); + /** Creates a user activity log entry. * * @param user for specified SEBServerUser instance diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java index 60b4d67f..b772188d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java @@ -161,6 +161,13 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { } @Override + @Transactional + public Result logArchive(final E entity) { + return log(UserLogActivityType.ARCHIVE, entity); + } + + @Override + @Transactional public Result logFinished(final E entity) { return log(UserLogActivityType.FINISHED, entity); } @@ -244,7 +251,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { try { log( - this.userService.getCurrentUser(), + this.userService.getCurrentUser().getUserInfo().uuid, activityType, entityType, entityId, @@ -272,7 +279,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { return Result.tryCatch(() -> { log( - this.userService.getCurrentUser(), + this.userService.getCurrentUser().getUserInfo().uuid, activityType, entityType, entityId, @@ -295,7 +302,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { _message = "Entity details: " + entity; } - log(user, activityType, entity.entityType(), entity.getModelId(), _message); + log(user.getUserInfo().uuid, activityType, entity.entityType(), entity.getModelId(), _message); return entity; }) .onError(TransactionHandler::rollback) @@ -308,8 +315,35 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { t)); } + @Override + @Transactional + public Result log( + final String userUUID, + final UserLogActivityType activityType, + final E entity, + final String message) { + + return Result.tryCatch(() -> { + String _message = message; + if (message == null) { + _message = "Entity details: " + entity; + } + + log(userUUID, activityType, entity.entityType(), entity.getModelId(), _message); + return entity; + }) + .onError(TransactionHandler::rollback) + .onError(t -> log.error( + "Unexpected error while trying to log user activity for user {}, action-type: {} entity-type: {} entity-id: {}", + userUUID, + activityType, + entity.entityType().name(), + entity.getModelId(), + t)); + } + private void log( - final SEBServerUser user, + final String userUUID, final UserLogActivityType activityType, final EntityType entityType, final String entityId, @@ -317,7 +351,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { this.userLogRecordMapper.insertSelective(new UserActivityLogRecord( null, - user.getUserInfo().uuid, + userUUID, System.currentTimeMillis(), activityType.name(), entityType.name(), diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 9b3df645..645da6b1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -193,7 +193,7 @@ public class ExamAdministrationController extends EntityController { return this.examDAO.byPK(modelId) .flatMap(this::checkWriteAccess) .flatMap(this.examAdminService::archiveExam) - .flatMap(this::logModify) + .flatMap(exam -> super.userActivityLogDAO.logArchive(exam)) .getOrThrow(); } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index a8789a59..6d6262d4 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -72,6 +72,7 @@ sebserver.overall.types.entityType.CERTIFICATE=Certificate sebserver.overall.types.entityType.EXAM_TEMPLATE=Exam Template sebserver.overall.types.entityType.EXAM_PROCTOR_DATA=Exam Proctoring Settings sebserver.overall.types.entityType.BATCH_ACTION=Batch Action +sebserver.overall.types.entityType.CLIENT_GROUP=Client Group sebserver.overall.activity.title.serveradmin=SEB Server Administration sebserver.overall.activity.title.sebconfig=Configurations @@ -513,6 +514,15 @@ sebserver.exam.list.empty=No Exam can be found. Please adapt the filter or impor sebserver.exam.list.modify.out.dated=Finished exams cannot be modified. sebserver.exam.list.action.no.modify.privilege=No Access: An Exam from another institution cannot be modified. +sebserver.exam.list.action.archive=Archive Selected Exams +sebserver.exam.list.action.delete=Delete Selected Exams +sebserver.exam.list.batch.archive.title=Batch Archive Exams +sebserver.exam.list.batch.action.archive=Archive All +sebserver.exam.list.batch.action.archive.info=This batch action archives all selected exams below.
If a particular exam is in a invalid state for archiving it will be ignored.
Please make sure that all selected exams shall be archived. +sebserver.exam.list.batch.delete.title=Delete All +sebserver.exam.list.batch.action.delete=Delete Selected Exams +sebserver.exam.list.batch.action.delete.info=This batch action deletes all selected exams below.
If a particular exam is in a invalid state for deleting it will be ignored.
Since this action is irreversible and all deleted exams are lost, please make sure that all selected exams shall be deleted and use Cancel to abort the action if not so. + sebserver.exam.consistency.title=Note: This exam is already running but has some missing settings sebserver.exam.consistency.missing-supporter= - There are no Exam Supporter defined for this exam. Use 'Edit Exam' on the right to add an Exam Supporter. sebserver.exam.consistency.missing-indicator= - There is no indicator defined for this exam. Use 'Add Indicator" on the right to add an indicator.