SEBSERV-160 implemented reset from template

This commit is contained in:
anhefti 2022-04-12 15:05:46 +02:00
parent 0921b713f5
commit 55baa2d518
45 changed files with 446 additions and 89 deletions

View file

@ -20,7 +20,7 @@ public final class API {
public enum BatchActionType {
EXAM_CONFIG_STATE_CHANGE(EntityType.CONFIGURATION_NODE),
EXAM_CONFIG_APPLY_TEMPLATE_VALUES(EntityType.CONFIGURATION_NODE);
EXAM_CONFIG_REST_TEMPLATE_SETTINGS(EntityType.CONFIGURATION_NODE);
public final EntityType entityType;

View file

@ -246,7 +246,7 @@ public class APIMessage implements Serializable {
}
public APIMessageException(final APIMessage apiMessage) {
super();
super(apiMessage.systemMessage + " " + apiMessage.details);
this.apiMessages = Arrays.asList(apiMessage);
}

View file

@ -18,6 +18,7 @@ public enum UserLogActivityType {
PASSWORD_CHANGE,
DEACTIVATE,
ACTIVATE,
FINISHED,
DELETE,
LOGIN,
LOGOUT

View file

@ -533,6 +533,12 @@ public enum ActionDefinition {
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,
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

@ -138,14 +138,14 @@ public class InstitutionList implements TemplateComposer {
.newAction(ActionDefinition.INSTITUTION_VIEW_FROM_LIST)
.withSelect(
table::getSelection,
table::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publish(false)
.newAction(ActionDefinition.INSTITUTION_MODIFY_FROM_LIST)
.withSelect(
table::getSelection,
table::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> instGrant.m(), false)

View file

@ -222,11 +222,12 @@ public class UserAccountList implements TemplateComposer {
.publishIf(userGrant::iw)
.newAction(ActionDefinition.USER_ACCOUNT_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.withSelect(table::getMultiSelection, PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publish(false)
.newAction(ActionDefinition.USER_ACCOUNT_MODIFY_FROM_LIST)
.withSelect(table::getSelection, this::editAction, EMPTY_SELECTION_TEXT_KEY)
.withSelect(table::getMultiSelection, this::editAction, EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im(), false)
.newAction(ActionDefinition.USER_ACCOUNT_TOGGLE_ACTIVITY)

View file

@ -244,7 +244,7 @@ public class UserActivityLogs implements TemplateComposer {
actionBuilder
.newAction(ActionDefinition.LOGS_USER_ACTIVITY_SHOW_DETAILS)
.withSelect(
table::getSelection,
table::getMultiSelection,
action -> this.showDetails(action, table.getSingleSelectedROWData()),
EMPTY_SELECTION_TEXT)
.noEventPropagation()

View file

@ -159,7 +159,7 @@ public class CertificateList implements TemplateComposer {
.newAction(ActionDefinition.SEB_CERTIFICATE_REMOVE)
.withConfirm(() -> FORM_ACTION_MESSAGE_REMOVE_CONFIRM_TEXT_KEY)
.withSelect(
table::getSelection,
table::getMultiSelection,
this::removeCertificate,
EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> grantCheck.iw(), false);

View file

@ -249,7 +249,7 @@ public class ConfigTemplateForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_TEMPLATE_ATTR_EDIT)
.withParentEntityKey(entityKey)
.withSelect(
attrTable::getSelection,
attrTable::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
EMPTY_ATTRIBUTE_SELECTION_TEXT_KEY)
.publishIf(() -> modifyGrant, false)
@ -257,7 +257,7 @@ public class ConfigTemplateForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_TEMPLATE_ATTR_SET_DEFAULT)
.withParentEntityKey(entityKey)
.withSelect(
attrTable::getSelection,
attrTable::getMultiSelection,
action -> this.resetToDefaults(action, attrTable),
EMPTY_ATTRIBUTE_SELECTION_TEXT_KEY)
.noEventPropagation()
@ -266,7 +266,7 @@ public class ConfigTemplateForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_TEMPLATE_ATTR_LIST_REMOVE_VIEW)
.withParentEntityKey(entityKey)
.withSelect(
attrTable::getSelection,
attrTable::getMultiSelection,
action -> this.removeFormView(action, attrTable),
EMPTY_ATTRIBUTE_SELECTION_TEXT_KEY)
.noEventPropagation()
@ -275,7 +275,7 @@ public class ConfigTemplateForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_TEMPLATE_ATTR_LIST_ATTACH_DEFAULT_VIEW)
.withParentEntityKey(entityKey)
.withSelect(
attrTable::getSelection,
attrTable::getMultiSelection,
action -> this.attachView(action, attrTable),
EMPTY_ATTRIBUTE_SELECTION_TEXT_KEY)
.noEventPropagation()

View file

@ -147,7 +147,7 @@ public class ConfigTemplateList implements TemplateComposer {
.publishIf(examConfigGrant::iw)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_TEMPLATE_VIEW_FROM_LIST)
.withSelect(templateTable::getSelection, PageAction::applySingleSelectionAsEntityKey,
.withSelect(templateTable::getMultiSelection, PageAction::applySingleSelectionAsEntityKey,
EMPTY_TEMPLATE_SELECTION_TEXT_KEY)
.publish(false)

View file

@ -173,7 +173,8 @@ public class SEBClientConfigList implements TemplateComposer {
.publishIf(clientConfigGrant::iw)
.newAction(ActionDefinition.SEB_CLIENT_CONFIG_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.withSelect(table::getMultiSelection, PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publish(false)
.newAction(ActionDefinition.SEB_CLIENT_CONFIG_MODIFY_FROM_LIST)

View file

@ -0,0 +1,95 @@
/*
* 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.content.configs;
import java.util.function.Supplier;
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.model.BatchAction;
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.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;
@Lazy
@Component
@GuiProfile
public class SEBExamConfigBatchResetToTemplatePopup extends AbstractBatchActionWizard {
private final static LocTextKey FORM_TITLE =
new LocTextKey("sebserver.examconfig.list.batch.reset.title");
private final static LocTextKey ACTION_DO_RESET =
new LocTextKey("sebserver.examconfig.list.batch.action.reset");
private final static LocTextKey FORM_INFO =
new LocTextKey("sebserver.examconfig.list.batch.action.reset.info");
protected SEBExamConfigBatchResetToTemplatePopup(
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.EXAM_CONFIG_REST_TEMPLATE_SETTINGS;
}
@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;
}
}

View file

@ -32,7 +32,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class SEBExamConfigStateChangePopup extends AbstractBatchActionWizard {
public class SEBExamConfigBatchStateChangePopup extends AbstractBatchActionWizard {
private static final String ATTR_SELECTED_TARGET_STATE = "selectedTargetState";
@ -45,7 +45,7 @@ public class SEBExamConfigStateChangePopup extends AbstractBatchActionWizard {
private final static LocTextKey FORM_STATUS_TEXT_KEY =
new LocTextKey("sebserver.examconfig.list.batch.action.status");
protected SEBExamConfigStateChangePopup(
protected SEBExamConfigBatchStateChangePopup(
final PageService pageService,
final ServerPushService serverPushService) {

View file

@ -70,7 +70,8 @@ public class SEBExamConfigList implements TemplateComposer {
private final PageService pageService;
private final SEBExamConfigImportPopup sebExamConfigImportPopup;
private final SEBExamConfigCreationPopup sebExamConfigCreationPopup;
private final SEBExamConfigStateChangePopup sebExamConfigStateChangePopup;
private final SEBExamConfigBatchStateChangePopup sebExamConfigBatchStateChangePopup;
private final SEBExamConfigBatchResetToTemplatePopup sebExamConfigBatchResetToTemplatePopup;
private final CurrentUser currentUser;
private final ResourceService resourceService;
private final int pageSize;
@ -79,13 +80,15 @@ public class SEBExamConfigList implements TemplateComposer {
final PageService pageService,
final SEBExamConfigImportPopup sebExamConfigImportPopup,
final SEBExamConfigCreationPopup sebExamConfigCreationPopup,
final SEBExamConfigStateChangePopup sebExamConfigStateChangePopup,
final SEBExamConfigBatchStateChangePopup sebExamConfigBatchStateChangePopup,
final SEBExamConfigBatchResetToTemplatePopup sebExamConfigBatchResetToTemplatePopup,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
this.sebExamConfigImportPopup = sebExamConfigImportPopup;
this.sebExamConfigCreationPopup = sebExamConfigCreationPopup;
this.sebExamConfigStateChangePopup = sebExamConfigStateChangePopup;
this.sebExamConfigBatchStateChangePopup = sebExamConfigBatchStateChangePopup;
this.sebExamConfigBatchResetToTemplatePopup = sebExamConfigBatchResetToTemplatePopup;
this.currentUser = pageService.getCurrentUser();
this.resourceService = pageService.getResourceService();
this.pageSize = pageSize;
@ -159,7 +162,8 @@ public class SEBExamConfigList implements TemplateComposer {
ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP_FROM_LIST,
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_STATE_CHANGE,
ActionDefinition.SEB_EXAM_CONFIG_BULK_RESET_TO_TEMPLATE))
.compose(pageContext.copyOf(content));
@ -171,7 +175,7 @@ public class SEBExamConfigList implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP_FROM_LIST)
.withSelect(
configTable::getSelection,
configTable::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publish(false)
@ -205,8 +209,16 @@ public class SEBExamConfigList implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_BULK_STATE_CHANGE)
.withSelect(
configTable.getGrantedSelection(this.currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
this.sebExamConfigStateChangePopup.popupCreationFunction(pageContext),
configTable::getMultiSelection,
this.sebExamConfigBatchStateChangePopup.popupCreationFunction(pageContext),
EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(() -> examConfigGrant.im(), false)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_BULK_RESET_TO_TEMPLATE)
.withSelect(
configTable::getMultiSelection,
this.sebExamConfigBatchResetToTemplatePopup.popupCreationFunction(pageContext),
EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(() -> examConfigGrant.im(), false)

View file

@ -133,7 +133,7 @@ public class ExamFormIndicators implements TemplateComposer {
.newAction(ActionDefinition.EXAM_INDICATOR_MODIFY_FROM_LIST)
.withParentEntityKey(entityKey)
.withSelect(
indicatorTable::getSelection,
indicatorTable::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> editable && indicatorTable.hasAnyContent(), false)
@ -141,7 +141,7 @@ public class ExamFormIndicators implements TemplateComposer {
.newAction(ActionDefinition.EXAM_INDICATOR_DELETE_FROM_LIST)
.withEntityKey(entityKey)
.withSelect(
indicatorTable::getSelection,
indicatorTable::getMultiSelection,
this::deleteSelectedIndicator,
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> editable && indicatorTable.hasAnyContent(), false)

View file

@ -210,7 +210,8 @@ public class ExamList implements TemplateComposer {
final GrantCheck userGrant = currentUser.grantCheck(EntityType.EXAM);
actionBuilder
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.withSelect(table::getMultiSelection, PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publish(false)
.newAction(ActionDefinition.EXAM_MODIFY_FROM_LIST)

View file

@ -268,7 +268,7 @@ public class ExamTemplateForm implements TemplateComposer {
.newAction(ActionDefinition.INDICATOR_TEMPLATE_MODIFY_FROM_LIST)
.withParentEntityKey(entityKey)
.withSelect(
indicatorTable::getSelection,
indicatorTable::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im() && indicatorTable.hasAnyContent(), false)
@ -276,7 +276,7 @@ public class ExamTemplateForm implements TemplateComposer {
.newAction(ActionDefinition.INDICATOR_TEMPLATE_DELETE_FROM_LIST)
.withEntityKey(entityKey)
.withSelect(
indicatorTable::getSelection,
indicatorTable::getMultiSelection,
this::deleteSelectedIndicator,
INDICATOR_EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im() && indicatorTable.hasAnyContent(), false)

View file

@ -175,11 +175,13 @@ public class ExamTemplateList implements TemplateComposer {
.publishIf(userGrant::iw)
.newAction(ActionDefinition.EXAM_TEMPLATE_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.withSelect(table::getMultiSelection, PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publish(false)
.newAction(ActionDefinition.EXAM_TEMPLATE_MODIFY_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.withSelect(table::getMultiSelection, PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> userGrant.im(), false);
}

View file

@ -169,7 +169,10 @@ public class LmsSetupList implements TemplateComposer {
.publishIf(userGrant::iw)
.newAction(ActionDefinition.LMS_SETUP_VIEW_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.withSelect(
table::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publish(false)
.newAction(ActionDefinition.LMS_SETUP_MODIFY_FROM_LIST)

View file

@ -236,7 +236,7 @@ public class QuizLookupList implements TemplateComposer {
actionBuilder
.newAction(ActionDefinition.QUIZ_DISCOVERY_SHOW_DETAILS)
.withSelect(
table::getSelection,
table::getMultiSelection,
action -> this.showDetails(
action,
table.getSingleSelectedROWData(),

View file

@ -170,7 +170,10 @@ public class FinishedExam implements TemplateComposer {
.newAction(ActionDefinition.VIEW_FINISHED_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(examKey)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.withSelect(
table::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publishIf(isExamSupporter, false);
}

View file

@ -141,7 +141,10 @@ public class FinishedExamList implements TemplateComposer {
actionBuilder
.newAction(ActionDefinition.VIEW_FINISHED_EXAM_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.withSelect(
table::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false);
}

View file

@ -264,7 +264,7 @@ public class MonitoringClientConnection implements TemplateComposer {
.withParentEntityKey(parentEntityKey)
.withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY)
.withSelect(
() -> notificationTable.getSelection(),
() -> notificationTable.getMultiSelection(),
action -> this.confirmNotification(action, connectionData, notificationTable),
NOTIFICATION_LIST_NO_SELECTION_KEY)

View file

@ -13,6 +13,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import org.eclipse.swt.SWT;
@ -172,7 +173,7 @@ public class MonitoringRunningExam implements TemplateComposer {
.withParentEntityKey(entityKey)
.create(),
this.pageService)
.withSelectionListener(this.pageService.getSelectionPublisher(
.withSelectionListener(this.getSelectionPublisherClientConnectionTable(
pageContext,
ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION,
ActionDefinition.MONITOR_EXAM_QUIT_SELECTED,
@ -505,4 +506,13 @@ public class MonitoringRunningExam implements TemplateComposer {
return action;
}
private Consumer<ClientConnectionTable> getSelectionPublisherClientConnectionTable(
final PageContext pageContext,
final ActionDefinition... actionDefinitions) {
return table -> this.pageService.firePageEvent(
new ActionActivationEvent(table.getSingleSelection() != null, actionDefinitions),
pageContext);
}
}

View file

@ -141,7 +141,10 @@ public class MonitoringRunningExamList implements TemplateComposer {
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_FROM_LIST)
.withSelect(table::getSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY)
.withSelect(
table::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false);
}

View file

@ -227,7 +227,7 @@ public class SEBClientEvents implements TemplateComposer {
actionBuilder
.newAction(ActionDefinition.LOGS_SEB_CLIENT_SHOW_DETAILS)
.withSelect(
table::getSelection,
table::getMultiSelection,
action -> this.sebClientEventDetailsPopup.showDetails(action, table.getSingleSelectedROWData()),
EMPTY_SELECTION_TEXT)
.noEventPropagation()

View file

@ -172,7 +172,7 @@ public interface PageService {
* @return a message supplier to notify deactivation dependencies to the user */
default <T extends Entity & Activatable> Supplier<LocTextKey> confirmDeactivation(final EntityTable<T> table) {
return () -> {
final List<EntityKey> multiSelection = table.getMultiSelection();
final Set<EntityKey> multiSelection = table.getMultiSelection();
if (multiSelection.size() > 1) {
throw new PageMessageException(MESSAGE_NO_MULTISELECTION);
}
@ -204,12 +204,12 @@ public interface PageService {
* @param pageContext the current PageContext
* @param actionDefinitions list of action definitions that activity should be toggled on table selection
* @return the selection publisher that handles this defines action activation on table selection */
default <T> Consumer<Set<T>> getSelectionPublisher(
default <T extends ModelIdAware> Consumer<EntityTable<T>> getSelectionPublisher(
final PageContext pageContext,
final ActionDefinition... actionDefinitions) {
return rows -> firePageEvent(
new ActionActivationEvent(!rows.isEmpty(), actionDefinitions),
return table -> firePageEvent(
new ActionActivationEvent(table.hasSelection(), actionDefinitions),
pageContext);
}
@ -225,15 +225,15 @@ public interface PageService {
* @param pageContext the current PageContext
* @param actionDefinitions list of action definitions that activity should be toggled on table selection
* @return the selection publisher that handles this defines action activation on table selection */
default <T extends Activatable> Consumer<Set<T>> getSelectionPublisher(
default <T extends Activatable & ModelIdAware> Consumer<EntityTable<T>> getSelectionPublisher(
final ActionDefinition toggle,
final ActionDefinition activate,
final ActionDefinition deactivate,
final PageContext pageContext,
final ActionDefinition... actionDefinitions) {
return rows -> {
return table -> {
final Set<T> rows = table.getPageSelectionData();
if (!rows.isEmpty()) {
firePageEvent(
new ActionActivationEvent(

View file

@ -171,7 +171,6 @@ public final class PageAction {
// if selection is needed, check selection fist, before confirm dialog
if (this.selectionSupplier != null) {
getMultiSelection();
}
final LocTextKey confirmMessage = this.confirm.apply(this);

View file

@ -249,7 +249,7 @@ public class PageServiceImpl implements PageService {
final Function<PageAction, PageAction> testBeforeActivation) {
return action -> {
final List<EntityKey> multiSelection = table.getMultiSelection();
final Set<EntityKey> multiSelection = table.getMultiSelection();
if (multiSelection == null || multiSelection.isEmpty()) {
throw new PageMessageException(noSelectionText);
}

View file

@ -88,7 +88,7 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
private final Table table;
private final ColorData colorData;
private final Function<ClientConnectionData, String> localizedClientConnectionStatusNameFunction;
private Consumer<Set<EntityKey>> selectionListener;
private Consumer<ClientConnectionTable> selectionListener;
private int tableWidth;
private boolean needsSort = false;
@ -238,7 +238,7 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
}
}
public ClientConnectionTable withSelectionListener(final Consumer<Set<EntityKey>> selectionListener) {
public ClientConnectionTable withSelectionListener(final Consumer<ClientConnectionTable> selectionListener) {
this.selectionListener = selectionListener;
return this;
}
@ -402,7 +402,7 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
return;
}
this.selectionListener.accept(this.getSelection());
this.selectionListener.accept(this);
}
private void notifyTableInfoClick(final Event event) {

View file

@ -95,7 +95,7 @@ public class EntityTable<ROW extends ModelIdAware> {
private final MultiValueMap<String, String> staticQueryParams;
private final BiConsumer<TableItem, ROW> rowDecorator;
private final Consumer<Set<ROW>> selectionListener;
private final Consumer<EntityTable<ROW>> selectionListener;
private final Consumer<Integer> contentChangeListener;
private final Set<String> multiselection;
@ -125,7 +125,7 @@ public class EntityTable<ROW extends ModelIdAware> {
final boolean hideNavigation,
final MultiValueMap<String, String> staticQueryParams,
final BiConsumer<TableItem, ROW> rowDecorator,
final Consumer<Set<ROW>> selectionListener,
final Consumer<EntityTable<ROW>> selectionListener,
final Consumer<Integer> contentChangeListener,
final String defaultSortColumn,
final PageSortOrder defaultSortOrder) {
@ -263,6 +263,10 @@ public class EntityTable<ROW extends ModelIdAware> {
return this.table.getItemCount() > 0;
}
public boolean hasSelection() {
return (this.multiselection != null && !this.multiselection.isEmpty()) || this.table.getSelectionCount() > 0;
}
public void setPageSize(final int pageSize) {
this.pageSize = pageSize;
updateTableRows(
@ -363,15 +367,18 @@ public class EntityTable<ROW extends ModelIdAware> {
return getEntityKey(selection[0]);
}
public List<EntityKey> getMultiSelection() {
public Set<EntityKey> getMultiSelection() {
if (this.multiselection == null) {
return Collections.emptyList();
return getPageSelectionData()
.stream()
.map(row -> new EntityKey(row.getModelId(), getEntityType()))
.collect(Collectors.toSet());
}
return this.multiselection
.stream()
.map(modelId -> new EntityKey(modelId, getEntityType()))
.collect(Collectors.toList());
.collect(Collectors.toSet());
}
public ROW getFirstRowData() {
@ -396,7 +403,7 @@ public class EntityTable<ROW extends ModelIdAware> {
return getRowData(selection[0]);
}
private Set<ROW> getPageSelectionData() {
public Set<ROW> getPageSelectionData() {
final TableItem[] selection = this.table.getSelection();
if (selection == null || selection.length == 0) {
return Collections.emptySet();
@ -407,10 +414,6 @@ public class EntityTable<ROW extends ModelIdAware> {
.collect(Collectors.toSet());
}
public Set<EntityKey> getSelection() {
return getSelection(null);
}
public Set<EntityKey> getSelection(final Predicate<ROW> grantCheck) {
final TableItem[] selection = this.table.getSelection();
if (selection == null) {
@ -685,7 +688,7 @@ public class EntityTable<ROW extends ModelIdAware> {
return;
}
this.selectionListener.accept(this.getPageSelectionData());
this.selectionListener.accept(this);
}
private void updateCurrentPageAttr() {
@ -807,6 +810,9 @@ public class EntityTable<ROW extends ModelIdAware> {
this.multiselection.remove(modelId);
} else {
this.multiselection.add(modelId);
Arrays.asList(this.table.getSelection())
.stream()
.forEach(i -> this.multiselection.add(getModelId(i)));
}
multiselectFromPage();
}

View file

@ -10,7 +10,6 @@ package ch.ethz.seb.sebserver.gui.table;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
@ -48,7 +47,7 @@ public class TableBuilder<ROW extends ModelIdAware> {
private boolean hideNavigation = false;
private Function<PageSupplier.Builder<ROW>, PageSupplier.Builder<ROW>> restCallAdapter;
private BiConsumer<TableItem, ROW> rowDecorator;
private Consumer<Set<ROW>> selectionListener;
private Consumer<EntityTable<ROW>> selectionListener;
private Consumer<Integer> contentChangeListener;
private boolean markupEnabled = false;
private String defaultSortColumn = null;
@ -152,7 +151,7 @@ public class TableBuilder<ROW extends ModelIdAware> {
return this;
}
public TableBuilder<ROW> withSelectionListener(final Consumer<Set<ROW>> selectionListener) {
public TableBuilder<ROW> withSelectionListener(final Consumer<EntityTable<ROW>> selectionListener) {
this.selectionListener = selectionListener;
return this;
}

View file

@ -201,6 +201,7 @@ public class PaginationServiceImpl implements PaginationService {
final PageSortOrder sortOrder = PageSortOrder.getSortOrder(sort);
final String sortColumnName = verifySortColumnName(sort, sortMappingName);
if (StringUtils.isNotBlank(sortColumnName)) {
startPage.setOrderByOnly(false);
switch (sortOrder) {
case DESCENDING: {
PageHelper.orderBy(sortColumnName + " DESC");
@ -211,6 +212,7 @@ public class PaginationServiceImpl implements PaginationService {
break;
}
}
PageHelper.orderBy("id");
}
}

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.authorization;
import java.security.Principal;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.WebDataBinder;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
@ -51,4 +52,9 @@ public interface UserService {
* @param binder Springs WebDataBinder is injected on controller side */
void addUsersInstitutionDefaultPropertySupport(final WebDataBinder binder);
/** Used to set authentication on different thread.
*
* @param authentication */
void setAuthenticationIfAbsent(Authentication authentication);
}

View file

@ -13,6 +13,7 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.stream.Collectors;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@ -26,7 +27,7 @@ import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
*
* This implements Spring's UserDetails and CredentialsContainer to act as a principal
* within internal authentication and authorization processes. */
public final class SEBServerUser implements UserDetails, CredentialsContainer {
public final class SEBServerUser implements UserDetails, CredentialsContainer, Authentication {
private static final long serialVersionUID = 5726250141482925769L;
@ -160,4 +161,34 @@ public final class SEBServerUser implements UserDetails, CredentialsContainer {
return new SEBServerUser(user.id, UserInfo.of(user.userInfo), user.password);
}
@Override
public String getName() {
return this.userInfo.username;
}
@Override
public Object getCredentials() {
return this;
}
@Override
public Object getDetails() {
return this;
}
@Override
public Object getPrincipal() {
return this;
}
@Override
public boolean isAuthenticated() {
return isEnabled();
}
@Override
public void setAuthenticated(final boolean isAuthenticated) throws IllegalArgumentException {
}
}

View file

@ -94,6 +94,13 @@ public class UserServiceImpl implements UserService {
binder.registerCustomEditor(Long.class, usersInstitutionDefaultEditor);
}
@Override
public void setAuthenticationIfAbsent(final Authentication authentication) {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// 1. OAuth2Authentication strategy
@Lazy
@Component
@ -115,6 +122,21 @@ public class UserServiceImpl implements UserService {
}
}
// 2. Separated thread strategy
@Lazy
@Component
public static class OtherThreadUserExtractStrategy implements ExtractUserFromAuthenticationStrategy {
@Override
public SEBServerUser extract(final Principal principal) {
if (principal instanceof SEBServerUser) {
return (SEBServerUser) principal;
}
return null;
}
}
private static final SEBServerUser ANONYMOUS_USER = new SEBServerUser(
-1L,
new UserInfo("SEB_SERVER_ANONYMOUS_USER", -2L, null, "anonymous", "anonymous", "anonymous", null, false,

View file

@ -22,6 +22,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.api.API;
@ -38,6 +40,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BatchActionServi
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.BatchActionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
@Service
@WebServiceProfile
@ -46,17 +50,23 @@ public class BatchActionServiceImpl implements BatchActionService {
private static final Logger log = LoggerFactory.getLogger(BatchActionServiceImpl.class);
private final BatchActionDAO batchActionDAO;
private final UserDAO userDAO;
private final TaskScheduler taskScheduler;
private final UserActivityLogDAO userActivityLogDAO;
private final EnumMap<BatchActionType, BatchActionExec> batchExecutions;
private ScheduledFuture<?> runningBatchProcess = null;
public BatchActionServiceImpl(
final BatchActionDAO batchActionDAO,
final UserDAO userDAO,
final UserActivityLogDAO userActivityLogDAO,
final Collection<BatchActionExec> batchExecutions,
final TaskScheduler taskScheduler) {
this.batchActionDAO = batchActionDAO;
this.userDAO = userDAO;
this.userActivityLogDAO = userActivityLogDAO;
this.taskScheduler = taskScheduler;
this.batchExecutions = new EnumMap<>(BatchActionType.class);
@ -158,7 +168,8 @@ public class BatchActionServiceImpl implements BatchActionService {
new BatchActionProcess(
new BatchActionHandlerImpl(action),
this.batchExecutions.get(action.actionType),
action),
action,
getAuthentication(action)),
Instant.now());
})
.onError(error -> {
@ -174,22 +185,43 @@ public class BatchActionServiceImpl implements BatchActionService {
}
}
private Authentication getAuthentication(final BatchAction action) {
try {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
return authentication;
}
return this.userDAO.byModelId(action.ownerId)
.flatMap(userInfo -> this.userDAO.sebServerUserByUsername(userInfo.username))
.onError(error -> log.error("Failed to get batch action owner user -> ", error))
.getOr(null);
} catch (final Exception e) {
log.error("Failed to get authentication: ", e);
return null;
}
}
private final static class BatchActionProcess implements Runnable {
private final BatchActionHandler batchActionHandler;
private final BatchActionExec batchActionExec;
private final BatchAction batchAction;
private final Authentication authentication;
private Set<String> processingIds;
public BatchActionProcess(
final BatchActionHandler batchActionHandler,
final BatchActionExec batchActionExec,
final BatchAction batchAction) {
final BatchAction batchAction,
final Authentication authentication) {
this.batchActionHandler = batchActionHandler;
this.batchActionExec = batchActionExec;
this.batchAction = batchAction;
this.authentication = authentication;
}
@Override
@ -198,6 +230,13 @@ public class BatchActionServiceImpl implements BatchActionService {
log.info("Starting or continuing batch action - {}", this.batchAction);
if (SecurityContextHolder.getContext().getAuthentication() == null) {
if (this.authentication == null) {
throw new IllegalStateException("No authentication found within batch context");
}
SecurityContextHolder.getContext().setAuthentication(this.authentication);
}
this.processingIds = new HashSet<>(this.batchAction.sourceIds);
this.processingIds.removeAll(this.batchAction.successful);
@ -256,10 +295,10 @@ public class BatchActionServiceImpl implements BatchActionService {
@Override
public void handleError(final String modelId, final Exception error) {
log.error(
"Failed to process single entity on batch action. ModelId: {}, action: ",
"Failed to process single entity on batch action. ModelId: {}, action: {}, errorMessage: {}",
modelId,
this.batchAction,
error);
error.getMessage());
BatchActionServiceImpl.this.batchActionDAO.setFailure(
this.batchAction.id,
@ -276,6 +315,10 @@ public class BatchActionServiceImpl implements BatchActionService {
.onError(error -> log.error(
"Failed to mark batch action as finished: {}",
this.batchAction, error));
BatchActionServiceImpl.this.batchActionDAO.byPK(this.batchAction.id)
.flatMap(BatchActionServiceImpl.this.userActivityLogDAO::logFinished)
.onError(error -> log.error("Failed to put audit log for batch action finish: ", error));
}
}

View file

@ -0,0 +1,86 @@
/*
* 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.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.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.BatchAction;
import ch.ethz.seb.sebserver.gbl.model.Entity;
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.sebconfig.ExamConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamConfigUpdateService;
@Lazy
@Component
@WebServiceProfile
public class ExamConfigResetToTemplate implements BatchActionExec {
private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigService sebExamConfigService;
private final ExamConfigUpdateService examConfigUpdateService;
private final AuthorizationService authorizationService;
public ExamConfigResetToTemplate(
final ConfigurationNodeDAO configurationNodeDAO,
final ExamConfigService sebExamConfigService,
final ExamConfigUpdateService examConfigUpdateService,
final AuthorizationService authorizationService) {
this.configurationNodeDAO = configurationNodeDAO;
this.sebExamConfigService = sebExamConfigService;
this.examConfigUpdateService = examConfigUpdateService;
this.authorizationService = authorizationService;
}
@Override
public BatchActionType actionType() {
return BatchActionType.EXAM_CONFIG_REST_TEMPLATE_SETTINGS;
}
@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(node -> this.authorizationService.check(PrivilegeType.MODIFY, node))
.map(this::checkConsistency)
.flatMap(this.sebExamConfigService::resetToTemplateSettings)
.map(node -> {
this.examConfigUpdateService
.processExamConfigurationChange(node.id)
.getOrThrow();
return node;
})
.map(Entity::getEntityKey);
}
private ConfigurationNode checkConsistency(final ConfigurationNode configurationNode) {
return configurationNode;
}
}

View file

@ -21,36 +21,31 @@ import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
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.UserDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService;
import io.micrometer.core.instrument.util.StringUtils;
@Lazy
@Component
@WebServiceProfile
public class SingleExamConfigStateChange implements BatchActionExec {
public class ExamConfigStateChange implements BatchActionExec {
private final ExamConfigService sebExamConfigService;
private final ConfigurationNodeDAO configurationNodeDAO;
private final AuthorizationService authorizationService;
private final UserDAO userDAO;
public SingleExamConfigStateChange(
public ExamConfigStateChange(
final ExamConfigService sebExamConfigService,
final ConfigurationNodeDAO configurationNodeDAO,
final AuthorizationService authorizationService,
final UserDAO userDAO) {
final AuthorizationService authorizationService) {
this.sebExamConfigService = sebExamConfigService;
this.configurationNodeDAO = configurationNodeDAO;
this.authorizationService = authorizationService;
this.userDAO = userDAO;
}
@Override
@ -71,13 +66,9 @@ public class SingleExamConfigStateChange implements BatchActionExec {
@Override
public Result<EntityKey> doSingleAction(final String modelId, final BatchAction batchAction) {
final UserInfo user = this.userDAO
.byModelId(batchAction.ownerId)
.getOrThrow();
return this.configurationNodeDAO
.byModelId(modelId)
.map(node -> this.authorizationService.check(PrivilegeType.MODIFY, user, node))
.flatMap(node -> this.authorizationService.check(PrivilegeType.MODIFY, node))
.map(node -> new ConfigurationNode(
node.id, null, null, null, null, null, null,
getTargetState(batchAction.attributes)))

View file

@ -77,6 +77,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 FINISHED
*
* @param entity the Entity
* @return Result of the Entity or referring to an Error if happened */
<E extends Entity> Result<E> logFinished(E entity);
/** Create a user activity log entry for the current user of activity type DELETE
*
* @param entity the Entity

View file

@ -160,6 +160,11 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
return log(UserLogActivityType.MODIFY, entity);
}
@Override
public <E extends Entity> Result<E> logFinished(final E entity) {
return log(UserLogActivityType.FINISHED, entity);
}
@Override
@Transactional
public <E extends Entity> Result<E> logDelete(final E entity) {

View file

@ -14,6 +14,7 @@ import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
@ -414,6 +415,18 @@ public class ExamConfigServiceImpl implements ExamConfigService {
"The Type of ConfigurationNode cannot change after creation");
}
// if configuration is in use, "Ready to Use" is not possible
if (configurationNode.status == ConfigurationStatus.READY_TO_USE) {
if (!this.examConfigurationMapDAO
.getExamIdsForConfigNodeId(configurationNode.id)
.getOr(Collections.emptyList())
.isEmpty()) {
throw new APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam configuration has references to at least one exam."));
}
}
// if changing to archived check possibility
if (configurationNode.status == ConfigurationStatus.ARCHIVED) {
if (existingNode.status != ConfigurationStatus.ARCHIVED) {

View file

@ -49,6 +49,7 @@ sebserver.overall.types.activityType.ACTIVATE=Activate
sebserver.overall.types.activityType.DELETE=Delete
sebserver.overall.types.activityType.LOGIN=Login
sebserver.overall.types.activityType.LOGOUT=Logout
sebserver.overall.types.activityType.FINISHED=Finished
sebserver.overall.types.entityType.CONFIGURATION_ATTRIBUTE=Configuration Attribute
sebserver.overall.types.entityType.CONFIGURATION_VALUE=Configuration Value
@ -70,6 +71,7 @@ sebserver.overall.types.entityType.EXAM_SEB_RESTRICTION=SEB Exam Restriction
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.activity.title.serveradmin=SEB Server Administration
sebserver.overall.activity.title.sebconfig=Configurations
@ -814,13 +816,16 @@ sebserver.examconfig.list.action.no.modify.privilege=No Access: An Exam Configur
sebserver.examconfig.action.list.new=Add Exam Configuration
sebserver.examconfig.action.list.view=View Exam Configuration
sebserver.examconfig.list.action.statechange=State Change
sebserver.examconfig.list.batch.statechange.title=Bulk State Change
sebserver.examconfig.list.batch.statechange.title=State Change All
sebserver.examconfig.list.batch.action.statechange=Change States
sebserver.examconfig.list.batch.progress=Bulk State Change in Progress: {0}%
sebserver.examconfig.list.batch.finished=Bulk State Change finished. Successful: {0}.Failed: {1}
sebserver.examconfig.list.batch.action.statechange.info=This action changes all selected exam configurations to a defined target state.<br/>Please note that this will be done only for those that can be changed to the defined target state in regard to its current state.
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.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
sebserver.examconfig.action.delete=Delete Exam Configuration

View file

@ -15,6 +15,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -248,11 +249,12 @@ public abstract class AdministrationAPIIntegrationTester {
}
protected String getOrderedUUIDs(final Collection<? extends Entity> list) {
return list
final List<String> l = list
.stream()
.map(userInfo -> userInfo.getModelId())
.collect(Collectors.toList())
.toString();
.collect(Collectors.toList());
l.sort((s1, s2) -> s1.compareTo(s2));
return l.toString();
}
}

View file

@ -268,7 +268,7 @@ public class UserAPITest extends AdministrationAPIIntegrationTester {
assertTrue(userInfos.numberOfPages == 1);
assertNotNull(userInfos.content);
assertTrue(userInfos.content.size() == 3);
assertEquals("[user5, user2, user1]", getOrderedUUIDs(userInfos.content));
assertEquals("[user1, user2, user5]", getOrderedUUIDs(userInfos.content));
}
@Test
@ -347,7 +347,7 @@ public class UserAPITest extends AdministrationAPIIntegrationTester {
assertTrue(userInfos.numberOfPages == 2);
assertNotNull(userInfos.content);
assertTrue(userInfos.content.size() == 3);
assertEquals("[user7, user6, user4]", getOrderedUUIDs(userInfos.content));
assertEquals("[user3, user4, user6]", getOrderedUUIDs(userInfos.content));
}
@Test