SEBSERV-353 implementation

This commit is contained in:
anhefti 2023-02-23 15:23:38 +01:00
parent f72e57d7c7
commit f380cd49f9
17 changed files with 656 additions and 18 deletions

View file

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

View file

@ -21,5 +21,6 @@ public enum UserLogActivityType {
FINISHED,
DELETE,
LOGIN,
LOGOUT
LOGOUT,
ARCHIVE
}

View file

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

View file

@ -69,7 +69,6 @@ public class SEBExamConfigBatchResetToTemplatePopup extends AbstractBatchActionW
protected Supplier<PageContext> createResultPageSupplier(
final PageContext pageContext,
final FormHandle<ConfigurationNode> 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<BatchAction>.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;
}

View file

@ -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<PageContext> createResultPageSupplier(
final PageContext pageContext,
final FormHandle<ConfigurationNode> formHandle) {
// No specific fields for this action
return () -> pageContext;
}
@Override
protected void extendBatchActionRequest(
final PageContext pageContext,
final RestCall<BatchAction>.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<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<Exam> 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);
}
}

View file

@ -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<PageContext> createResultPageSupplier(
final PageContext pageContext,
final FormHandle<ConfigurationNode> formHandle) {
// No specific fields for this action
return () -> pageContext;
}
@Override
protected void extendBatchActionRequest(final PageContext pageContext,
final RestCall<BatchAction>.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<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<Exam> 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);
}
}

View file

@ -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<Exam> 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<Exam, String> examLmsSetupNameFunction(final ResourceService resourceService) {
public static Function<Exam, String> examLmsSetupNameFunction(final ResourceService resourceService) {
return exam -> resourceService.getLmsSetupNameFunction()
.apply(String.valueOf(exam.lmsSetupId));
}

View file

@ -84,6 +84,12 @@ public abstract class AbstractBatchActionWizard {
final FormBuilder formHead,
final boolean readonly);
protected void applySelectionList(
final PageContext formContext,
final Set<EntityKey> multiSelection) {
}
public Function<PageAction, PageAction> popupCreationFunction(final PageContext pageContext) {
return action -> {
@ -135,6 +141,8 @@ public abstract class AbstractBatchActionWizard {
false)
.build();
applySelectionList(formContext, multiSelection);
return createResultPageSupplier(pageContext, formHandle);
}

View file

@ -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<Collection<Exam>> {
public GetExamsByIds() {
super(new TypeKey<>(
CallType.GET_LIST,
EntityType.EXAM,
new TypeReference<Collection<Exam>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.LIST_PATH_SEGMENT);
}
}

View file

@ -101,11 +101,13 @@ public class StaticListPageSupplier<T> implements PageSupplier<T> {
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) {

View file

@ -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<String, String> actionAttributes) {
// no additional check here
return null;
}
@Override
public Result<EntityKey> 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<Exam> checkWriteAccess(final Exam entity) {
if (entity != null) {
this.authorization.checkWrite(entity);
}
return Result.of(entity);
}
private Result<Exam> logArchived(final Exam entity, final BatchAction batchAction) {
return this.userActivityLogDAO.log(
batchAction.ownerId,
UserLogActivityType.ARCHIVE,
entity,
"Part of batch action: " + batchAction.processorId);
}
}

View file

@ -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<String, String> actionAttributes) {
// no additional check here
return null;
}
@Override
public Result<EntityKey> 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<Exam> deleteExamWithRefs(final Exam entity) {
final Result<Collection<EntityKey>> 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<Exam> checkWriteAccess(final Exam entity) {
if (entity != null) {
this.authorization.checkWrite(entity);
}
return Result.of(entity);
}
private Result<Exam> 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<Exam> logDeleted(final Exam entity, final BatchAction batchAction) {
return this.userActivityLogDAO.log(
batchAction.ownerId,
UserLogActivityType.DELETE,
entity,
"Part of batch action: " + batchAction.processorId);
}
}

View file

@ -76,7 +76,6 @@ public class ExamConfigResetToTemplate implements BatchActionExec {
return node;
})
.map(Entity::getEntityKey);
}
private ConfigurationNode checkConsistency(final ConfigurationNode configurationNode) {

View file

@ -78,6 +78,12 @@ public interface UserActivityLogDAO extends
* @return Result of the Entity or referring to an Error if happened */
<E extends Entity> Result<E> 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 */
<E extends Entity> Result<E> 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 */
<E extends Entity> Result<E> log(
String userUUID,
UserLogActivityType activityType,
E entity,
String message);
/** Creates a user activity log entry.
*
* @param user for specified SEBServerUser instance

View file

@ -161,6 +161,13 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
}
@Override
@Transactional
public <E extends Entity> Result<E> logArchive(final E entity) {
return log(UserLogActivityType.ARCHIVE, entity);
}
@Override
@Transactional
public <E extends Entity> Result<E> 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 <E extends Entity> Result<E> 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(),

View file

@ -193,7 +193,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return this.examDAO.byPK(modelId)
.flatMap(this::checkWriteAccess)
.flatMap(this.examAdminService::archiveExam)
.flatMap(this::logModify)
.flatMap(exam -> super.userActivityLogDAO.logArchive(exam))
.getOrThrow();
}

View file

@ -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.<br/>If a particular exam is in a invalid state for archiving it will be ignored.<br/>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.<br/>If a particular exam is in a invalid state for deleting it will be ignored.<br/>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.