From b5704dea95b4eff8b98b9c2aac055ec228d9b400 Mon Sep 17 00:00:00 2001 From: anhefti Date: Tue, 13 Sep 2022 09:36:01 +0200 Subject: [PATCH] SEBSERV-163 finished grouping with filter --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 1 + .../sebserver/gbl/model/exam/ClientGroup.java | 7 +- .../model/session/ClientConnectionData.java | 23 +++ .../MonitoringSEBConnectionData.java | 16 +- .../gui/content/action/ActionCategory.java | 3 +- .../gui/content/action/ActionDefinition.java | 28 ++-- .../gui/content/action/ActionPane.java | 42 ++--- .../monitoring/MonitoringRunningExam.java | 151 +++++++++++++++--- .../session/ClientConnectionTable.java | 8 +- .../session/FullPageMonitoringGUIUpdate.java | 2 +- .../session/FullPageMonitoringUpdate.java | 120 ++++++++++++-- ...oringStatus.java => MonitoringFilter.java} | 32 +++- .../dao/impl/ClientGroupDAOImpl.java | 1 + .../session/impl/ExamSessionCacheService.java | 2 + .../session/impl/ExamSessionServiceImpl.java | 61 +++++-- .../impl/SEBClientConnectionServiceImpl.java | 6 +- .../api/ExamMonitoringController.java | 92 ++++++----- src/main/resources/messages.properties | 6 +- 18 files changed, 459 insertions(+), 142 deletions(-) rename src/main/java/ch/ethz/seb/sebserver/gui/service/session/{MonitoringStatus.java => MonitoringFilter.java} (72%) 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 c41c76d3..e8f203c3 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 @@ -197,6 +197,7 @@ public final class API { public static final String EXAM_MONITORING_NOTIFICATION_ENDPOINT = "/notification"; public static final String EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT = "/disable-connection"; public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states"; + public static final String EXAM_MONITORING_CLIENT_GROUP_FILTER = "hidden-client-group"; public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams"; public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT = "/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ClientGroup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ClientGroup.java index 804887b9..d6f8ecb3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ClientGroup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ClientGroup.java @@ -28,7 +28,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain.CLIENT_GROUP; import ch.ethz.seb.sebserver.gbl.util.Utils; @JsonIgnoreProperties(ignoreUnknown = true) -public class ClientGroup implements ClientGroupData { +public class ClientGroup implements ClientGroupData, Comparable { public static final String FILTER_ATTR_EXAM_ID = "examId"; @@ -285,4 +285,9 @@ public class ClientGroup implements ClientGroupData { return true; } + @Override + public int compareTo(final ClientGroup o) { + return o == null ? -1 : this.id.compareTo(o.id); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java index c1d8b45c..d6e08505 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java @@ -84,6 +84,29 @@ public class ClientConnectionData implements GrantEntity { return this.groups != null && this.groups.contains(clientGroupId); } + @JsonIgnore + public boolean containsAllClientGroup(final Set clientGroupIds) { + if (this.groups == null || clientGroupIds == null || clientGroupIds.isEmpty()) { + return false; + } + return this.groups != null && this.groups.containsAll(clientGroupIds); + } + + @JsonIgnore + public boolean filter(final Set clientGroupIdsToHide) { + if (this.groups == null || clientGroupIdsToHide == null || clientGroupIdsToHide.isEmpty()) { + return true; + } + + for (final Long id : this.groups) { + if (!clientGroupIdsToHide.contains(id)) { + return true; + } + } + + return false; + } + @Override public EntityType entityType() { return this.clientConnection.entityType(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringSEBConnectionData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringSEBConnectionData.java index c8714be4..832b3cc3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringSEBConnectionData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringSEBConnectionData.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gbl.monitoring; import java.util.Arrays; import java.util.Collection; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -25,6 +26,7 @@ public class MonitoringSEBConnectionData { public static final String ATTR_CONNECTIONS = "connections"; public static final String ATTR_STATUS_MAPPING = "statusMapping"; + public static final String ATTR_CLIENT_GROUP_MAPPING = "clientGroupMapping"; @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) public final Long examId; @@ -32,16 +34,20 @@ public class MonitoringSEBConnectionData { public final Collection connections; @JsonProperty(ATTR_STATUS_MAPPING) public final int[] connectionsPerStatus; + @JsonProperty(ATTR_CLIENT_GROUP_MAPPING) + public final Map connectionsPerClientGroup; @JsonCreator public MonitoringSEBConnectionData( @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId, @JsonProperty(ATTR_CONNECTIONS) final Collection connections, - @JsonProperty(ATTR_STATUS_MAPPING) final int[] connectionsPerStatus) { + @JsonProperty(ATTR_STATUS_MAPPING) final int[] connectionsPerStatus, + @JsonProperty(ATTR_CLIENT_GROUP_MAPPING) final Map connectionsPerClientGroup) { this.examId = examId; this.connections = connections; this.connectionsPerStatus = connectionsPerStatus; + this.connectionsPerClientGroup = connectionsPerClientGroup; } public Long getExamId() { @@ -64,6 +70,14 @@ public class MonitoringSEBConnectionData { return this.connectionsPerStatus[status.code]; } + @JsonIgnore + public int getNumberOfConnection(final Long clientGroupId) { + if (this.connectionsPerClientGroup == null || !this.connectionsPerClientGroup.containsKey(clientGroupId)) { + return -1; + } + return this.connectionsPerClientGroup.get(clientGroupId); + } + @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java index 7a029614..2662b34a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java @@ -42,7 +42,8 @@ public enum ActionCategory { LOGS_USER_ACTIVITY_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1), LOGS_SEB_CLIENT_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1), VARIA(new LocTextKey("sebserver.overall.action.category.varia"), 0), - FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.filter"), 50), + STATE_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.statefilter"), 40), + GROUP_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.groupfilter"), 50), PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.proctoring"), 60), FINISHED_EXAM_LIST(new LocTextKey("sebserver.finished.exam.list.actions"), 1); 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 5485fc37..135ddac8 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 @@ -882,44 +882,54 @@ public enum ActionDefinition { new LocTextKey("sebserver.monitoring.exam.connection.action.hide.requested"), ImageIcon.TOGGLE_OFF, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, - ActionCategory.FILTER), + ActionCategory.STATE_FILTER), MONITOR_EXAM_SHOW_REQUESTED_CONNECTION( new LocTextKey("sebserver.monitoring.exam.connection.action.show.requested"), ImageIcon.TOGGLE_ON, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, - ActionCategory.FILTER), + ActionCategory.STATE_FILTER), MONITOR_EXAM_HIDE_ACTIVE_CONNECTION( new LocTextKey("sebserver.monitoring.exam.connection.action.hide.active"), ImageIcon.TOGGLE_OFF, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, - ActionCategory.FILTER), + ActionCategory.STATE_FILTER), MONITOR_EXAM_SHOW_ACTIVE_CONNECTION( new LocTextKey("sebserver.monitoring.exam.connection.action.show.active"), ImageIcon.TOGGLE_ON, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, - ActionCategory.FILTER), - + ActionCategory.STATE_FILTER), MONITOR_EXAM_HIDE_CLOSED_CONNECTION( new LocTextKey("sebserver.monitoring.exam.connection.action.hide.closed"), ImageIcon.TOGGLE_OFF, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, - ActionCategory.FILTER), + ActionCategory.STATE_FILTER), MONITOR_EXAM_SHOW_CLOSED_CONNECTION( new LocTextKey("sebserver.monitoring.exam.connection.action.show.closed"), ImageIcon.TOGGLE_ON, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, - ActionCategory.FILTER), + ActionCategory.STATE_FILTER), MONITOR_EXAM_HIDE_DISABLED_CONNECTION( new LocTextKey("sebserver.monitoring.exam.connection.action.hide.disabled"), ImageIcon.TOGGLE_OFF, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, - ActionCategory.FILTER), + ActionCategory.STATE_FILTER), MONITOR_EXAM_SHOW_DISABLED_CONNECTION( new LocTextKey("sebserver.monitoring.exam.connection.action.show.disabled"), ImageIcon.TOGGLE_ON, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, - ActionCategory.FILTER), + ActionCategory.STATE_FILTER), + + MONITOR_EXAM_HIDE_CLIENT_GROUP_CONNECTION( + new LocTextKey("sebserver.monitoring.exam.connection.action.hide.clientgroup"), + ImageIcon.TOGGLE_OFF, + PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, + ActionCategory.GROUP_FILTER), + MONITOR_EXAM_SHOW_CLIENT_GROUP_CONNECTION( + new LocTextKey("sebserver.monitoring.exam.connection.action.show.clientgroup"), + ImageIcon.TOGGLE_ON, + PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, + ActionCategory.GROUP_FILTER), MONITORING_EXAM_SEARCH_CONNECTIONS( new LocTextKey("sebserver.monitoring.search.action"), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionPane.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionPane.java index f7165ec5..961b5fb5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionPane.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionPane.java @@ -247,26 +247,7 @@ public class ActionPane implements TemplateComposer { actionsTitle.setData(RWT.CUSTOM_VARIANT, "close"); actionsTitle.setImage(WidgetFactory.ImageIcon.ACTIVE.getImage(parent.getDisplay())); actionsTitle.setText("    " + titleText); - actionsTitle.addListener(SWT.MouseUp, event -> { - try { - final Control contentControl = composite.getChildren()[1]; - if (contentControl.isVisible()) { - actionsTitle.setData(RWT.CUSTOM_VARIANT, "open"); - contentControl.setVisible(false); - final GridData l = (GridData) contentControl.getLayoutData(); - l.heightHint = 0; - composite.getParent().layout(true, true); - } else { - actionsTitle.setData(RWT.CUSTOM_VARIANT, "close"); - contentControl.setVisible(true); - final GridData l = (GridData) contentControl.getLayoutData(); - l.heightHint = SWT.DEFAULT; - composite.getParent().layout(true, true); - } - } catch (final Exception e) { - // just ignore - } - }); + actionsTitle.addListener(SWT.MouseUp, event -> actionGroupExpand(composite, actionsTitle)); } actionsTitle.setLayoutData(titleLayout); @@ -319,6 +300,27 @@ public class ActionPane implements TemplateComposer { return actions; } + private void actionGroupExpand(final Composite composite, final Label actionsTitle) { + try { + final Control contentControl = composite.getChildren()[1]; + if (contentControl.isVisible()) { + actionsTitle.setData(RWT.CUSTOM_VARIANT, "open"); + contentControl.setVisible(false); + final GridData l = (GridData) contentControl.getLayoutData(); + l.heightHint = 0; + composite.getParent().layout(true, true); + } else { + actionsTitle.setData(RWT.CUSTOM_VARIANT, "close"); + contentControl.setVisible(true); + final GridData l = (GridData) contentControl.getLayoutData(); + l.heightHint = SWT.DEFAULT; + composite.getParent().layout(true, true); + } + } catch (final Exception e) { + // just ignore + } + } + private void clearDisposedTrees(final Map actionTrees) { new ArrayList<>(actionTrees.entrySet()) .forEach(entry -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java index 08c44e6b..126669b7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java @@ -11,6 +11,8 @@ package ch.ethz.seb.sebserver.gui.content.monitoring; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.function.BooleanSupplier; import java.util.function.Consumer; @@ -64,7 +66,7 @@ import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; import ch.ethz.seb.sebserver.gui.service.session.FullPageMonitoringGUIUpdate; import ch.ethz.seb.sebserver.gui.service.session.FullPageMonitoringUpdate; import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; -import ch.ethz.seb.sebserver.gui.service.session.MonitoringStatus; +import ch.ethz.seb.sebserver.gui.service.session.MonitoringFilter; import ch.ethz.seb.sebserver.gui.service.session.proctoring.MonitoringProctoringService; import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService; @@ -258,6 +260,7 @@ public class MonitoringRunningExam implements TemplateComposer { if (isExamSupporter.getAsBoolean()) { guiUpdates.add(createFilterActions( + clientGroups, fullPageMonitoringUpdate, actionBuilder, clientTable, @@ -347,13 +350,14 @@ public class MonitoringRunningExam implements TemplateComposer { } private FullPageMonitoringGUIUpdate createFilterActions( - final MonitoringStatus monitoringStatus, + final Collection clientGroups, + final MonitoringFilter monitoringStatus, final PageActionBuilder actionBuilder, final ClientConnectionTable clientTable, final BooleanSupplier isExamSupporter) { - final StatusFilterGUIUpdate statusFilterGUIUpdate = - new StatusFilterGUIUpdate(this.pageService.getPolyglotPageService()); + final FilterGUIUpdate statusFilterGUIUpdate = + new FilterGUIUpdate(this.pageService.getPolyglotPageService()); addFilterAction( monitoringStatus, @@ -388,61 +392,118 @@ public class MonitoringRunningExam implements TemplateComposer { ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION, ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION); + if (clientGroups != null && !clientGroups.isEmpty()) { + clientGroups.forEach(clientGroup -> { + + addClientGroupFilterAction( + monitoringStatus, + statusFilterGUIUpdate, + actionBuilder, + clientTable, + clientGroup, + ActionDefinition.MONITOR_EXAM_SHOW_CLIENT_GROUP_CONNECTION, + ActionDefinition.MONITOR_EXAM_HIDE_CLIENT_GROUP_CONNECTION); + + }); + } + return statusFilterGUIUpdate; } private void addFilterAction( - final MonitoringStatus monitoringStatus, - final StatusFilterGUIUpdate statusFilterGUIUpdate, + final MonitoringFilter filter, + final FilterGUIUpdate filterGUIUpdate, final PageActionBuilder actionBuilder, final ClientConnectionTable clientTable, final ConnectionStatus status, final ActionDefinition showActionDef, final ActionDefinition hideActionDef) { - final int numOfConnections = monitoringStatus.getNumOfConnections(status); - if (monitoringStatus.isStatusHidden(status)) { - final PageAction showAction = actionBuilder.newAction(showActionDef) - .withExec(showStateViewAction(monitoringStatus, clientTable, status)) + final int numOfConnections = filter.getNumOfConnections(status); + PageAction action; + if (filter.isStatusHidden(status)) { + action = actionBuilder.newAction(showActionDef) + .withExec(showStateViewAction(filter, clientTable, status)) .noEventPropagation() .withSwitchAction( actionBuilder.newAction(hideActionDef) .withExec( - hideStateViewAction(monitoringStatus, clientTable, status)) + hideStateViewAction(filter, clientTable, status)) .noEventPropagation() .withNameAttributes(numOfConnections) .create()) .withNameAttributes(numOfConnections) .create(); - this.pageService.publishAction( - showAction, - treeItem -> statusFilterGUIUpdate.register(treeItem, status)); } else { - final PageAction hideAction = actionBuilder.newAction(hideActionDef) - .withExec(hideStateViewAction(monitoringStatus, clientTable, status)) + action = actionBuilder.newAction(hideActionDef) + .withExec(hideStateViewAction(filter, clientTable, status)) .noEventPropagation() .withSwitchAction( actionBuilder.newAction(showActionDef) .withExec( - showStateViewAction(monitoringStatus, clientTable, status)) + showStateViewAction(filter, clientTable, status)) .noEventPropagation() .withNameAttributes(numOfConnections) .create()) .withNameAttributes(numOfConnections) .create(); - this.pageService.publishAction( - hideAction, - treeItem -> statusFilterGUIUpdate.register(treeItem, status)); } + this.pageService.publishAction( + action, + treeItem -> filterGUIUpdate.register(treeItem, status)); + } + + private void addClientGroupFilterAction( + final MonitoringFilter filter, + final FilterGUIUpdate filterGUIUpdate, + final PageActionBuilder actionBuilder, + final ClientConnectionTable clientTable, + final ClientGroup clientGroup, + final ActionDefinition showActionDef, + final ActionDefinition hideActionDef) { + + final int numOfConnections = filter.getNumOfConnections(clientGroup.id); + PageAction action; + if (filter.isClientGroupHidden(clientGroup.id)) { + action = actionBuilder.newAction(showActionDef) + .withExec(showClientGroupAction(filter, clientTable, clientGroup.id)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(hideActionDef) + .withExec( + hideClientGroupViewAction(filter, clientTable, clientGroup.id)) + .noEventPropagation() + .withNameAttributes(clientGroup.name, numOfConnections) + .create()) + .withNameAttributes(clientGroup.name, numOfConnections) + .create(); + } else { + action = actionBuilder.newAction(hideActionDef) + .withExec(hideClientGroupViewAction(filter, clientTable, clientGroup.id)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(showActionDef) + .withExec( + showClientGroupAction(filter, clientTable, clientGroup.id)) + .noEventPropagation() + .withNameAttributes(clientGroup.name, numOfConnections) + .create()) + .withNameAttributes(clientGroup.name, numOfConnections) + .create(); + } + this.pageService.publishAction( + action, + treeItem -> filterGUIUpdate.register(treeItem, clientGroup.id)); } /** This holds the filter action items and implements the specific GUI update for it */ - private class StatusFilterGUIUpdate implements FullPageMonitoringGUIUpdate { + private class FilterGUIUpdate implements FullPageMonitoringGUIUpdate { private final PolyglotPageService polyglotPageService; private final TreeItem[] actionItemPerStateFilter = new TreeItem[ConnectionStatus.values().length]; + private final Map actionItemPerClientGroup = new HashMap<>(); - public StatusFilterGUIUpdate(final PolyglotPageService polyglotPageService) { + public FilterGUIUpdate(final PolyglotPageService polyglotPageService) { this.polyglotPageService = polyglotPageService; } @@ -450,8 +511,12 @@ public class MonitoringRunningExam implements TemplateComposer { this.actionItemPerStateFilter[status.code] = item; } + void register(final TreeItem item, final Long clientGroupId) { + this.actionItemPerClientGroup.put(clientGroupId, item); + } + @Override - public void update(final MonitoringStatus monitoringStatus) { + public void update(final MonitoringFilter monitoringStatus) { final ConnectionStatus[] states = ConnectionStatus.values(); for (int i = 0; i < states.length; i++) { final ConnectionStatus state = states[i]; @@ -463,6 +528,18 @@ public class MonitoringRunningExam implements TemplateComposer { this.polyglotPageService.injectI18n(treeItem, action.getTitle()); } } + + if (!this.actionItemPerClientGroup.isEmpty()) { + this.actionItemPerClientGroup.entrySet().stream().forEach(entry -> { + final int numOfConnections = monitoringStatus.getNumOfConnections(entry.getKey()); + if (numOfConnections >= 0) { + final TreeItem treeItem = entry.getValue(); + final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY); + action.setTitleArgument(1, numOfConnections); + this.polyglotPageService.injectI18n(treeItem, action.getTitle()); + } + }); + } } } @@ -472,7 +549,7 @@ public class MonitoringRunningExam implements TemplateComposer { } private static Function showStateViewAction( - final MonitoringStatus monitoringStatus, + final MonitoringFilter monitoringStatus, final ClientConnectionTable clientTable, final ConnectionStatus status) { @@ -484,7 +561,7 @@ public class MonitoringRunningExam implements TemplateComposer { } private static Function hideStateViewAction( - final MonitoringStatus monitoringStatus, + final MonitoringFilter monitoringStatus, final ClientConnectionTable clientTable, final ConnectionStatus status) { @@ -495,6 +572,30 @@ public class MonitoringRunningExam implements TemplateComposer { }; } + private static Function showClientGroupAction( + final MonitoringFilter monitoringStatus, + final ClientConnectionTable clientTable, + final Long clientGroupId) { + + return action -> { + monitoringStatus.showClientGroup(clientGroupId); + clientTable.removeSelection(); + return action; + }; + } + + private static Function hideClientGroupViewAction( + final MonitoringFilter monitoringStatus, + final ClientConnectionTable clientTable, + final Long clientGroupId) { + + return action -> { + monitoringStatus.hideClientGroup(clientGroupId); + clientTable.removeSelection(); + return action; + }; + } + private Set selectionForInstruction(final ClientConnectionTable clientTable) { final Set connectionTokens = clientTable.getConnectionTokens( cc -> cc.status.clientActiveStatus, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java index 46a89254..a7a85494 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java @@ -312,10 +312,10 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate } @Override - public void update(final MonitoringStatus monitoringStatus) { + public void update(final MonitoringFilter monitoringStatus) { final Collection connectionData = monitoringStatus.getConnectionData(); final boolean sizeChanged = connectionData.size() != this.table.getItemCount(); - final boolean needsSync = monitoringStatus.statusFilterChanged() || + final boolean needsSync = monitoringStatus.filterChanged() || this.forceUpdateAll || sizeChanged || (this.tableMapping != null && @@ -350,7 +350,7 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate } } }); - monitoringStatus.resetStatusFilterChanged(); + monitoringStatus.resetFilterChanged(); this.toDelete.clear(); } @@ -624,7 +624,7 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate private String getGroupInfo() { final StringBuilder sb = new StringBuilder(); ClientConnectionTable.this.clientGroupMapping.keySet().stream().forEach(key -> { - if (this.connectionData.groups.contains(key)) { + if (this.connectionData.groups != null && this.connectionData.groups.contains(key)) { final ClientGroup clientGroup = ClientConnectionTable.this.clientGroupMapping.get(key); sb.append(WidgetFactory.getTextWithBackgroundHTML(clientGroup.name, clientGroup.color)); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/FullPageMonitoringGUIUpdate.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/FullPageMonitoringGUIUpdate.java index e9c14a1a..f92b1b8d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/FullPageMonitoringGUIUpdate.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/FullPageMonitoringGUIUpdate.java @@ -11,6 +11,6 @@ package ch.ethz.seb.sebserver.gui.service.session; @FunctionalInterface public interface FullPageMonitoringGUIUpdate { - void update(MonitoringStatus monitoringStatus); + void update(MonitoringFilter monitoringStatus); } \ No newline at end of file diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/FullPageMonitoringUpdate.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/FullPageMonitoringUpdate.java index 6cf7f546..d1369653 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/FullPageMonitoringUpdate.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/FullPageMonitoringUpdate.java @@ -10,7 +10,10 @@ package ch.ethz.seb.sebserver.gui.service.session; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.eclipse.swt.widgets.Composite; @@ -20,6 +23,8 @@ import org.slf4j.LoggerFactory; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.async.AsyncRunner; +import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; import ch.ethz.seb.sebserver.gbl.util.Utils; @@ -29,6 +34,7 @@ import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext; import ch.ethz.seb.sebserver.gui.service.push.ServerPushService; import ch.ethz.seb.sebserver.gui.service.push.UpdateErrorHandler; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.clientgroup.GetClientGroups; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetMonitoringFullPageData; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2RestTemplateException; @@ -36,11 +42,12 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2Re * full page monitoring. * * This handles server push and GUI update and also implements kind of circuit breaker and error handling */ -public class FullPageMonitoringUpdate implements MonitoringStatus { +public class FullPageMonitoringUpdate implements MonitoringFilter { static final Logger log = LoggerFactory.getLogger(FullPageMonitoringUpdate.class); - private static final String USER_SESSION_STATUS_FILTER_ATTRIBUTE = "USER_SESSION_STATUS_FILTER_ATTRIBUTE"; + private static final String USER_SESSION_STATUS_FILTER_ATTRIBUTE = "USER_SESSION_STATUS_FILTER"; + private static final String USER_SESSION_GROUP_FILTER_ATTRIBUTE = "USER_SESSION_GROUP_FILTER"; private final ServerPushService serverPushService; private final PageService pageService; @@ -49,9 +56,13 @@ public class FullPageMonitoringUpdate implements MonitoringStatus { private final Collection guiUpdates; private ServerPushContext pushContext; + private final EnumSet statusFilter; private String statusFilterParam = ""; - private boolean statusFilterChanged = false; + private final Set clientGroupFilter; + private String clientGroupFilterParam = ""; + private boolean filterChanged = false; + private boolean updateInProgress = false; private MonitoringFullPageData monitoringFullPageData = null; @@ -72,7 +83,19 @@ public class FullPageMonitoringUpdate implements MonitoringStatus { this.guiUpdates = guiUpdates; this.statusFilter = EnumSet.noneOf(ConnectionStatus.class); - loadStatusFilter(); + loadFilter(); + + final Collection clientGroups = pageService.getRestService() + .getBuilder(GetClientGroups.class) + .withQueryParam(Indicator.FILTER_ATTR_EXAM_ID, String.valueOf(examId)) + .call() + .getOr(Collections.emptyList()); + + if (clientGroups != null && !clientGroups.isEmpty()) { + this.clientGroupFilter = new HashSet<>(); + } else { + this.clientGroupFilter = null; + } } public void start(final PageContext pageContext, final Composite anchor, final long pollInterval) { @@ -105,13 +128,13 @@ public class FullPageMonitoringUpdate implements MonitoringStatus { } @Override - public boolean statusFilterChanged() { - return this.statusFilterChanged; + public boolean filterChanged() { + return this.filterChanged; } @Override - public void resetStatusFilterChanged() { - this.statusFilterChanged = false; + public void resetFilterChanged() { + this.filterChanged = false; } @Override @@ -122,13 +145,43 @@ public class FullPageMonitoringUpdate implements MonitoringStatus { @Override public void hideStatus(final ConnectionStatus status) { this.statusFilter.add(status); - saveStatusFilter(); + saveFilter(); } @Override public void showStatus(final ConnectionStatus status) { this.statusFilter.remove(status); - saveStatusFilter(); + saveFilter(); + } + + @Override + public boolean hasClientGroupFilter() { + return this.clientGroupFilter != null; + } + + @Override + public boolean isClientGroupHidden(final Long clientGroupId) { + return this.clientGroupFilter != null && this.clientGroupFilter.contains(clientGroupId); + } + + @Override + public void hideClientGroup(final Long clientGroupId) { + if (this.clientGroupFilter == null) { + return; + } + + this.clientGroupFilter.add(clientGroupId); + saveFilter(); + } + + @Override + public void showClientGroup(final Long clientGroupId) { + if (this.clientGroupFilter == null) { + return; + } + + this.clientGroupFilter.remove(clientGroupId); + saveFilter(); } @Override @@ -161,8 +214,15 @@ public class FullPageMonitoringUpdate implements MonitoringStatus { } private void updateBusinessData() { - this.monitoringFullPageData = this.restCallBuilder - .withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam) + + RestCall.RestCallBuilder restCallBuilder = this.restCallBuilder + .withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam); + if (hasClientGroupFilter()) { + restCallBuilder = restCallBuilder + .withHeader(API.EXAM_MONITORING_CLIENT_GROUP_FILTER, this.clientGroupFilterParam); + } + + this.monitoringFullPageData = restCallBuilder .call() .get(error -> { recoverFromDisposedRestTemplate(error); @@ -182,22 +242,32 @@ public class FullPageMonitoringUpdate implements MonitoringStatus { }); } - private void saveStatusFilter() { + private void saveFilter() { try { this.pageService .getCurrentUser() .putAttribute( USER_SESSION_STATUS_FILTER_ATTRIBUTE, StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR)); + if (hasClientGroupFilter()) { + this.pageService + .getCurrentUser() + .putAttribute( + USER_SESSION_GROUP_FILTER_ATTRIBUTE, + StringUtils.join(this.clientGroupFilter, Constants.LIST_SEPARATOR)); + } } catch (final Exception e) { log.warn("Failed to save status filter to user session"); } finally { this.statusFilterParam = StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR); - this.statusFilterChanged = true; + if (hasClientGroupFilter()) { + this.clientGroupFilterParam = StringUtils.join(this.clientGroupFilter, Constants.LIST_SEPARATOR); + } + this.filterChanged = true; } } - private void loadStatusFilter() { + private void loadFilter() { try { final String attribute = this.pageService .getCurrentUser() @@ -206,17 +276,33 @@ public class FullPageMonitoringUpdate implements MonitoringStatus { if (attribute != null) { Arrays.asList(StringUtils.split(attribute, Constants.LIST_SEPARATOR)) .forEach(name -> this.statusFilter.add(ConnectionStatus.valueOf(name))); - } else { this.statusFilter.add(ConnectionStatus.DISABLED); } + + if (hasClientGroupFilter()) { + final String groups = this.pageService + .getCurrentUser() + .getAttribute(USER_SESSION_GROUP_FILTER_ATTRIBUTE); + this.statusFilter.clear(); + if (groups != null) { + Arrays.asList(StringUtils.split(groups, Constants.LIST_SEPARATOR)) + .forEach(id -> this.clientGroupFilter.add(Long.parseLong(id))); + } + } } catch (final Exception e) { log.warn("Failed to load status filter to user session"); this.statusFilter.clear(); this.statusFilter.add(ConnectionStatus.DISABLED); + if (hasClientGroupFilter()) { + this.clientGroupFilter.clear(); + } } finally { this.statusFilterParam = StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR); - this.statusFilterChanged = true; + if (hasClientGroupFilter()) { + this.clientGroupFilterParam = StringUtils.join(this.clientGroupFilter, Constants.LIST_SEPARATOR); + } + this.filterChanged = true; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringStatus.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java similarity index 72% rename from src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringStatus.java rename to src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java index 4443431d..782fd785 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringStatus.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java @@ -12,21 +12,22 @@ import java.util.Collection; import java.util.Collections; import java.util.EnumSet; +import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; -import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; -import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; +import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData; -public interface MonitoringStatus { +public interface MonitoringFilter { EnumSet getStatusFilter(); String getStatusFilterParam(); - boolean statusFilterChanged(); + boolean filterChanged(); - void resetStatusFilterChanged(); + void resetFilterChanged(); boolean isStatusHidden(ConnectionStatus status); @@ -34,6 +35,18 @@ public interface MonitoringStatus { void showStatus(ConnectionStatus status); + boolean hasClientGroupFilter(); + + default boolean isClientGroupHidden(final ClientGroup clientGroup) { + return isClientGroupHidden(clientGroup.id); + } + + boolean isClientGroupHidden(Long clientGroupId); + + void hideClientGroup(Long clientGroupId); + + void showClientGroup(Long clientGroupId); + MonitoringFullPageData getMonitoringFullPageData(); default MonitoringSEBConnectionData getMonitoringSEBConnectionData() { @@ -63,6 +76,15 @@ public interface MonitoringStatus { } } + default int getNumOfConnections(final Long clientGroupId) { + final MonitoringSEBConnectionData monitoringSEBConnectionData = getMonitoringSEBConnectionData(); + if (monitoringSEBConnectionData != null) { + return monitoringSEBConnectionData.getNumberOfConnection(clientGroupId); + } else { + return 0; + } + } + default Collection proctoringData() { final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData(); if (monitoringFullPageData != null) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientGroupDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientGroupDAOImpl.java index c7d7d6cf..f44d862a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientGroupDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientGroupDAOImpl.java @@ -194,6 +194,7 @@ public class ClientGroupDAOImpl implements ClientGroupDAO { .stream() .map(this::toDomainModel) .flatMap(DAOLoggingSupport::logAndSkipOnError) + .sorted() .collect(Collectors.toList())); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java index a81f8304..aada30f9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java @@ -39,6 +39,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService @WebServiceProfile public class ExamSessionCacheService { + public static final Object CLIENT_CONECTION_CREATION_LOCK = new Object(); + public static final String CACHE_NAME_RUNNING_EXAM = "RUNNING_EXAM"; public static final String CACHE_NAME_ACTIVE_CLIENT_CONNECTION = "ACTIVE_CLIENT_CONNECTION"; public static final String CACHE_NAME_SEB_CONFIG_EXAM = "SEB_CONFIG_EXAM"; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index 8098bf0e..40c44d9a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -13,7 +13,9 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; @@ -34,6 +36,7 @@ import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage; import ch.ethz.seb.sebserver.gbl.model.Entity; +import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; @@ -43,6 +46,7 @@ import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientGroupDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; @@ -64,6 +68,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { private final IndicatorDAO indicatorDAO; private final ExamSessionCacheService examSessionCacheService; private final ExamDAO examDAO; + private final ClientGroupDAO clientGroupDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO; private final CacheManager cacheManager; private final SEBRestrictionService sebRestrictionService; @@ -76,6 +81,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { protected ExamSessionServiceImpl( final ExamSessionCacheService examSessionCacheService, final ExamDAO examDAO, + final ClientGroupDAO clientGroupDAO, final ExamConfigurationMapDAO examConfigurationMapDAO, final ClientConnectionDAO clientConnectionDAO, final IndicatorDAO indicatorDAO, @@ -87,6 +93,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { this.examSessionCacheService = examSessionCacheService; this.examDAO = examDAO; + this.clientGroupDAO = clientGroupDAO; this.examConfigurationMapDAO = examConfigurationMapDAO; this.clientConnectionDAO = clientConnectionDAO; this.cacheManager = cacheManager; @@ -350,7 +357,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { @Override public ClientConnectionDataInternal getConnectionDataInternal(final String connectionToken) { - synchronized (this.examSessionCacheService) { + synchronized (ExamSessionCacheService.CLIENT_CONECTION_CREATION_LOCK) { return this.examSessionCacheService.getClientConnection(connectionToken); } } @@ -405,25 +412,33 @@ public class ExamSessionServiceImpl implements ExamSessionService { for (int i = 0; i < statusMapping.length; i++) { statusMapping[i] = 0; } + // needed to store connection numbers per client group too + final Collection groups = this.clientGroupDAO.allForExam(examId).getOr(null); + final Map clientGroupMapping = (groups != null && !groups.isEmpty()) + ? new HashMap<>() + : null; updateClientConnections(examId); - synchronized (this.examSessionCacheService) { - final List filteredConnections = this.clientConnectionDAO - .getConnectionTokens(examId) - .getOrThrow() - .stream() - .map(token -> this.examSessionCacheService.getClientConnection(token)) - .filter(Objects::nonNull) - .map(c -> { - statusMapping[c.clientConnection.status.code]++; - return c; - }) - .filter(filter) - .collect(Collectors.toList()); + final List filteredConnections = this.clientConnectionDAO + .getConnectionTokens(examId) + .getOrThrow() + .stream() + .map(token -> getConnectionDataInternal(token)) + .filter(Objects::nonNull) + .map(c -> { + statusMapping[c.clientConnection.status.code]++; + processClientGroupMapping(c.groups, clientGroupMapping); + return c; + }) + .filter(filter) + .collect(Collectors.toList()); - return new MonitoringSEBConnectionData(examId, filteredConnections, statusMapping); - } + return new MonitoringSEBConnectionData( + examId, + filteredConnections, + statusMapping, + clientGroupMapping); }); } @@ -557,4 +572,18 @@ public class ExamSessionServiceImpl implements ExamSessionService { && Objects.equals(exam.name, runningExam.name); } + private void processClientGroupMapping(final Set groups, final Map clientGroupMapping) { + if (groups == null || clientGroupMapping == null) { + return; + } + + groups.forEach(id -> { + if (clientGroupMapping.containsKey(id)) { + clientGroupMapping.put(id, clientGroupMapping.get(id) + 1); + } else { + clientGroupMapping.put(id, 1); + } + }); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java index e54fef71..4eef5111 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java @@ -692,8 +692,10 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic private void processPing(final String connectionToken, final long timestamp, final int pingNumber) { - final ClientConnectionDataInternal activeClientConnection = - this.examSessionCacheService.getClientConnection(connectionToken); + ClientConnectionDataInternal activeClientConnection = null; + synchronized (ExamSessionCacheService.CLIENT_CONECTION_CREATION_LOCK) { + activeClientConnection = this.examSessionCacheService.getClientConnection(connectionToken); + } if (activeClientConnection != null) { activeClientConnection.notifyPing(timestamp, pingNumber); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index f9c007dd..31b5a4b0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -11,7 +11,9 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Predicate; @@ -248,31 +250,17 @@ public class ExamMonitoringController { required = true, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, - @RequestHeader(name = API.EXAM_MONITORING_STATE_FILTER, required = false) final String hiddenStates) { + @RequestHeader(name = API.EXAM_MONITORING_STATE_FILTER, required = false) final String hiddenStates, + @RequestHeader( + name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER, + required = false) final String hiddenClientGroups) { checkPrivileges(institutionId, examId); - final EnumSet filterStates = EnumSet.noneOf(ConnectionStatus.class); - if (StringUtils.isNoneBlank(hiddenStates)) { - final String[] split = StringUtils.split(hiddenStates, Constants.LIST_SEPARATOR); - for (int i = 0; i < split.length; i++) { - filterStates.add(ConnectionStatus.valueOf(split[i])); - } - } - - final boolean active = filterStates.contains(ConnectionStatus.ACTIVE); - if (active) { - filterStates.remove(ConnectionStatus.ACTIVE); - } - return this.examSessionService .getMonitoringSEBConnectionsData( examId, - filterStates.isEmpty() - ? Objects::nonNull - : active - ? withActiveFilter(filterStates) - : noneActiveFilter(filterStates)) + createMonitoringFilter(hiddenStates, hiddenClientGroups)) .getOrThrow().connections; } @@ -288,31 +276,17 @@ public class ExamMonitoringController { required = true, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, - @RequestHeader(name = API.EXAM_MONITORING_STATE_FILTER, required = false) final String hiddenStates) { + @RequestHeader(name = API.EXAM_MONITORING_STATE_FILTER, required = false) final String hiddenStates, + @RequestHeader( + name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER, + required = false) final String hiddenClientGroups) { final Exam runningExam = checkPrivileges(institutionId, examId); - final EnumSet filterStates = EnumSet.noneOf(ConnectionStatus.class); - if (StringUtils.isNoneBlank(hiddenStates)) { - final String[] split = StringUtils.split(hiddenStates, Constants.LIST_SEPARATOR); - for (int i = 0; i < split.length; i++) { - filterStates.add(ConnectionStatus.valueOf(split[i])); - } - } - - final boolean active = filterStates.contains(ConnectionStatus.ACTIVE); - if (active) { - filterStates.remove(ConnectionStatus.ACTIVE); - } - final MonitoringSEBConnectionData monitoringSEBConnectionData = this.examSessionService .getMonitoringSEBConnectionsData( examId, - filterStates.isEmpty() - ? Objects::nonNull - : active - ? withActiveFilter(filterStates) - : noneActiveFilter(filterStates)) + createMonitoringFilter(hiddenStates, hiddenClientGroups)) .getOrThrow(); if (this.examAdminService.isProctoringEnabled(runningExam).getOr(false)) { @@ -509,4 +483,46 @@ public class ExamMonitoringController { }; } + private Predicate createMonitoringFilter( + final String hiddenStates, + final String hiddenClientGroups) { + + final EnumSet filterStates = EnumSet.noneOf(ConnectionStatus.class); + if (StringUtils.isNotBlank(hiddenStates)) { + final String[] split = StringUtils.split(hiddenStates, Constants.LIST_SEPARATOR); + for (int i = 0; i < split.length; i++) { + filterStates.add(ConnectionStatus.valueOf(split[i])); + } + } + + final boolean active = filterStates.contains(ConnectionStatus.ACTIVE); + if (active) { + filterStates.remove(ConnectionStatus.ACTIVE); + } + + final Predicate stateFilter = filterStates.isEmpty() + ? Objects::nonNull + : active + ? withActiveFilter(filterStates) + : noneActiveFilter(filterStates); + + Set filterClientGroups = null; + if (StringUtils.isNotBlank(hiddenClientGroups)) { + filterClientGroups = new HashSet<>(); + final String[] split = StringUtils.split(hiddenClientGroups, Constants.LIST_SEPARATOR); + for (int i = 0; i < split.length; i++) { + filterClientGroups.add(Long.parseLong(split[i])); + } + } + + final Set _filterClientGroups = filterClientGroups; + final Predicate filter = ccd -> { + if (ccd == null) { + return false; + } + return stateFilter.test(ccd) && ccd.filter(_filterClientGroups); + }; + return filter; + } + } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index fd7e3c63..19fe13b6 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1885,7 +1885,8 @@ sebserver.monitoring.exam.list.actions= sebserver.monitoring.exam.action.detail.view=Back To Monitoring sebserver.monitoring.exam.action.list.view=Monitoring sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} ) -sebserver.exam.monitoring.action.category.filter=Filter +sebserver.exam.monitoring.action.category.statefilter=State Filter +sebserver.exam.monitoring.action.category.groupfilter=Client Group Filter sebserver.exam.overall.action.category.proctoring=Proctoring sebserver.monitoring.exam.action.proctoring.openTownhall=Open Townhall sebserver.monitoring.exam.action.proctoring.showTownhall=Show Townhall @@ -1971,6 +1972,8 @@ sebserver.monitoring.exam.connection.action.hide.disabled=Hide Canceled ( {0} ) sebserver.monitoring.exam.connection.action.show.disabled=Show Canceled ( {0} ) sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined ( {0} ) sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined ( {0} ) +sebserver.monitoring.exam.connection.action.hide.clientgroup=Hide {0} ( {1} ) +sebserver.monitoring.exam.connection.action.show.clientgroup=Show {0} ( {1} ) sebserver.monitoring.exam.connection.action.proctoring=Single Room Proctoring sebserver.monitoring.exam.connection.action.proctoring.examroom=Exam Room Proctoring sebserver.monitoring.exam.connection.action.openTownhall.confirm=You are about to open the town-hall room and force all SEB clients to join the town-hall room.
Are you sure to open the town-hall? @@ -1978,7 +1981,6 @@ sebserver.monitoring.exam.connection.action.closeTownhall.confirm=You are about sebserver.monitoring.exam.connection.action.singleroom.confirm=You are about to open the single/one to one room for this participant.
Are you sure you want to open the single room? sebserver.monitoring.exam.connection.actions.group2=  sebserver.monitoring.exam.connection.actions.group3=  - sebserver.monitoring.exam.connection.notificationlist.actions= sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification sebserver.monitoring.exam.connection.action.confirm.notification.text=Are you sure you want to confirm this pending notification?

Note that this will send a notification confirmation instruction to the SEB client and remove this notification from the pending list.