Merge pull request #105 from SafeExamBrowser/SEBSERV-478

add issue filters to exam monitoring
This commit is contained in:
Andreas Hefti 2024-01-25 16:03:43 +01:00 committed by GitHub
commit 474eb4f062
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 317 additions and 17 deletions

View file

@ -219,6 +219,8 @@ public final class API {
public static final String EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT = "/signature"; public static final String EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT = "/signature";
public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states"; 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_CLIENT_GROUP_FILTER = "hidden-client-group";
public static final String EXAM_MONITORING_ISSUE_FILTER = "hidden-issues";
public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams"; public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams";
public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT = public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT =
"/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}"; "/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}";

View file

@ -57,6 +57,17 @@ public final class ClientConnection implements GrantEntity {
} }
} }
public enum ConnectionIssueStatus {
ASK_GRANTED(0),
SEB_VERSION_GRANTED(1);
public final int code;
ConnectionIssueStatus(final int code){
this.code = code;
}
}
public final static List<String> ACTIVE_STATES = Utils.immutableListOf( public final static List<String> ACTIVE_STATES = Utils.immutableListOf(
ConnectionStatus.ACTIVE.name(), ConnectionStatus.ACTIVE.name(),
ConnectionStatus.READY.name(), ConnectionStatus.READY.name(),

View file

@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionIssueStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringData; import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringDataView; import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringDataView;
@ -27,6 +28,7 @@ public class MonitoringSEBConnectionData {
public static final String ATTR_CONNECTIONS = "cons"; public static final String ATTR_CONNECTIONS = "cons";
public static final String ATTR_STATUS_MAPPING = "sm"; public static final String ATTR_STATUS_MAPPING = "sm";
public static final String ATTR_CLIENT_GROUP_MAPPING = "cgm"; public static final String ATTR_CLIENT_GROUP_MAPPING = "cgm";
public static final String ATTR_ISSUE_MAPPING = "im";
@JsonProperty(ATTR_CONNECTIONS) @JsonProperty(ATTR_CONNECTIONS)
public final Collection<? extends ClientMonitoringDataView> monitoringData; public final Collection<? extends ClientMonitoringDataView> monitoringData;
@ -37,25 +39,33 @@ public class MonitoringSEBConnectionData {
@JsonProperty(ATTR_CLIENT_GROUP_MAPPING) @JsonProperty(ATTR_CLIENT_GROUP_MAPPING)
public final Map<Long, Integer> connectionsPerClientGroup; public final Map<Long, Integer> connectionsPerClientGroup;
@JsonProperty(ATTR_ISSUE_MAPPING)
public final int[] connectionPerIssue;
@JsonCreator @JsonCreator
public MonitoringSEBConnectionData( public MonitoringSEBConnectionData(
@JsonProperty(ATTR_CONNECTIONS) final Collection<ClientMonitoringData> connections, @JsonProperty(ATTR_CONNECTIONS) final Collection<ClientMonitoringData> connections,
@JsonProperty(ATTR_STATUS_MAPPING) final int[] connectionsPerStatus, @JsonProperty(ATTR_STATUS_MAPPING) final int[] connectionsPerStatus,
@JsonProperty(ATTR_ISSUE_MAPPING) final int[] connectionPerIssue,
@JsonProperty(ATTR_CLIENT_GROUP_MAPPING) final Map<Long, Integer> connectionsPerClientGroup) { @JsonProperty(ATTR_CLIENT_GROUP_MAPPING) final Map<Long, Integer> connectionsPerClientGroup) {
this.monitoringData = connections; this.monitoringData = connections;
this.connectionsPerStatus = connectionsPerStatus; this.connectionsPerStatus = connectionsPerStatus;
this.connectionPerIssue = connectionPerIssue;
this.connectionsPerClientGroup = connectionsPerClientGroup; this.connectionsPerClientGroup = connectionsPerClientGroup;
} }
public MonitoringSEBConnectionData( public MonitoringSEBConnectionData(
final int[] connectionsPerStatus, final int[] connectionsPerStatus,
final Map<Long, Integer> connectionsPerClientGroup, final Map<Long, Integer> connectionsPerClientGroup,
final int[] connectionsPerIssue,
final Collection<? extends ClientMonitoringDataView> connections) { final Collection<? extends ClientMonitoringDataView> connections) {
this.monitoringData = connections;
this.connectionsPerStatus = connectionsPerStatus; this.connectionsPerStatus = connectionsPerStatus;
this.connectionsPerClientGroup = connectionsPerClientGroup; this.connectionsPerClientGroup = connectionsPerClientGroup;
this.connectionPerIssue = connectionsPerIssue;
this.monitoringData = connections;
} }
public Collection<? extends ClientMonitoringDataView> getMonitoringData() { public Collection<? extends ClientMonitoringDataView> getMonitoringData() {
@ -82,6 +92,14 @@ public class MonitoringSEBConnectionData {
return this.connectionsPerClientGroup.get(clientGroupId); return this.connectionsPerClientGroup.get(clientGroupId);
} }
@JsonIgnore
public int getNumberOfConnection(final ConnectionIssueStatus connectionIssueStatus) {
if (this.connectionPerIssue == null || this.connectionPerIssue.length <= connectionIssueStatus.code) {
return -1;
}
return this.connectionPerIssue[connectionIssueStatus.code];
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();

View file

@ -47,8 +47,9 @@ public enum ActionCategory {
VARIA(new LocTextKey("sebserver.overall.action.category.varia"), 0), VARIA(new LocTextKey("sebserver.overall.action.category.varia"), 0),
STATE_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.statefilter"), 40), STATE_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.statefilter"), 40),
GROUP_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.groupfilter"), 50), GROUP_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.groupfilter"), 50),
PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.proctoring"), 60), ISSUE_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.issuefilter"), 60),
SCREEN_PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.screenproctoring"), 65), PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.proctoring"), 70),
SCREEN_PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.screenproctoring"), 75),
FINISHED_EXAM_LIST(new LocTextKey("sebserver.finished.exam.list.actions"), 1); FINISHED_EXAM_LIST(new LocTextKey("sebserver.finished.exam.list.actions"), 1);

View file

@ -1030,6 +1030,30 @@ public enum ActionDefinition {
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.GROUP_FILTER), ActionCategory.GROUP_FILTER),
MONITOR_EXAM_HIDE_ASK_GRANTED(
new LocTextKey("sebserver.monitoring.exam.connection.action.hide.askgranted"),
ImageIcon.TOGGLE_OFF,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.ISSUE_FILTER),
MONITOR_EXAM_SHOW_ASK_GRANTED(
new LocTextKey("sebserver.monitoring.exam.connection.action.show.askgranted"),
ImageIcon.TOGGLE_ON,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.ISSUE_FILTER),
MONITOR_EXAM_HIDE_SEB_VERSION_GRANTED(
new LocTextKey("sebserver.monitoring.exam.connection.action.hide.sebversiongranted"),
ImageIcon.TOGGLE_OFF,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.ISSUE_FILTER),
MONITOR_EXAM_SHOW_SEB_VERSION_GRANTED(
new LocTextKey("sebserver.monitoring.exam.connection.action.show.sebversiongranted"),
ImageIcon.TOGGLE_ON,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.ISSUE_FILTER),
MONITORING_EXAM_SEARCH_CONNECTIONS( MONITORING_EXAM_SEARCH_CONNECTIONS(
new LocTextKey("sebserver.monitoring.search.action"), new LocTextKey("sebserver.monitoring.search.action"),
ImageIcon.SEARCH, ImageIcon.SEARCH,

View file

@ -43,6 +43,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionIssueStatus;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
@ -437,6 +438,23 @@ public class MonitoringRunningExam implements TemplateComposer {
ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION, ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION,
ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION); ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION);
addIssueFilterAction(
monitoringStatus,
statusFilterGUIUpdate,
actionBuilder,
clientTable,
ConnectionIssueStatus.ASK_GRANTED,
ActionDefinition.MONITOR_EXAM_SHOW_ASK_GRANTED,
ActionDefinition.MONITOR_EXAM_HIDE_ASK_GRANTED);
addIssueFilterAction(
monitoringStatus,
statusFilterGUIUpdate,
actionBuilder,
clientTable,
ConnectionIssueStatus.SEB_VERSION_GRANTED,
ActionDefinition.MONITOR_EXAM_SHOW_SEB_VERSION_GRANTED,
ActionDefinition.MONITOR_EXAM_HIDE_SEB_VERSION_GRANTED);
if (clientGroups != null && !clientGroups.isEmpty()) { if (clientGroups != null && !clientGroups.isEmpty()) {
clientGroups.forEach(clientGroup -> { clientGroups.forEach(clientGroup -> {
@ -455,6 +473,34 @@ public class MonitoringRunningExam implements TemplateComposer {
return statusFilterGUIUpdate; return statusFilterGUIUpdate;
} }
private void addIssueFilterAction(
final MonitoringFilter filter,
final FilterGUIUpdate filterGUIUpdate,
final PageActionBuilder actionBuilder,
final ClientConnectionTable clientTable,
final ConnectionIssueStatus connectionIssueStatus,
final ActionDefinition showActionDef,
final ActionDefinition hideActionDef) {
final int numOfConnections = filter.getNumOfConnections(connectionIssueStatus);
final PageAction action = actionBuilder.newAction(hideActionDef)
.withExec(hideIssueViewAction(filter, clientTable, connectionIssueStatus))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(showActionDef)
.withExec(showIssueViewAction(filter, clientTable, connectionIssueStatus))
.noEventPropagation()
.withNameAttributes(numOfConnections)
.create())
.withNameAttributes(numOfConnections)
.create();
this.pageService.publishAction(
action,
treeItem -> filterGUIUpdate.register(treeItem, connectionIssueStatus));
}
private void addFilterAction( private void addFilterAction(
final MonitoringFilter filter, final MonitoringFilter filter,
final FilterGUIUpdate filterGUIUpdate, final FilterGUIUpdate filterGUIUpdate,
@ -544,6 +590,7 @@ public class MonitoringRunningExam implements TemplateComposer {
private final PolyglotPageService polyglotPageService; private final PolyglotPageService polyglotPageService;
private final TreeItem[] actionItemPerStateFilter = new TreeItem[ConnectionStatus.values().length]; private final TreeItem[] actionItemPerStateFilter = new TreeItem[ConnectionStatus.values().length];
private final TreeItem[] actionItemPerIssueFilter = new TreeItem[ConnectionIssueStatus.values().length];
private final Map<Long, TreeItem> actionItemPerClientGroup = new HashMap<>(); private final Map<Long, TreeItem> actionItemPerClientGroup = new HashMap<>();
public FilterGUIUpdate(final PolyglotPageService polyglotPageService) { public FilterGUIUpdate(final PolyglotPageService polyglotPageService) {
@ -558,6 +605,10 @@ public class MonitoringRunningExam implements TemplateComposer {
this.actionItemPerClientGroup.put(clientGroupId, item); this.actionItemPerClientGroup.put(clientGroupId, item);
} }
void register(final TreeItem item, final ConnectionIssueStatus status) {
this.actionItemPerIssueFilter[status.code] = item;
}
@Override @Override
public void update(final MonitoringFilter monitoringStatus) { public void update(final MonitoringFilter monitoringStatus) {
final ConnectionStatus[] states = ConnectionStatus.values(); final ConnectionStatus[] states = ConnectionStatus.values();
@ -572,6 +623,19 @@ public class MonitoringRunningExam implements TemplateComposer {
} }
} }
final ConnectionIssueStatus[] connectionIssueStates = ConnectionIssueStatus.values();
for (int i = 0; i < connectionIssueStates.length; i++) {
final ConnectionIssueStatus state = connectionIssueStates[i];
final int numOfConnections = monitoringStatus.getNumOfConnections(state);
if (numOfConnections >= 0 && this.actionItemPerIssueFilter[state.code] != null) {
final TreeItem treeItem = this.actionItemPerIssueFilter[state.code];
final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY);
action.setTitleArgument(0, numOfConnections);
this.polyglotPageService.injectI18n(treeItem, action.getTitle());
}
}
if (!this.actionItemPerClientGroup.isEmpty()) { if (!this.actionItemPerClientGroup.isEmpty()) {
this.actionItemPerClientGroup.entrySet().stream().forEach(entry -> { this.actionItemPerClientGroup.entrySet().stream().forEach(entry -> {
final int numOfConnections = monitoringStatus.getNumOfConnections(entry.getKey()); final int numOfConnections = monitoringStatus.getNumOfConnections(entry.getKey());
@ -639,6 +703,32 @@ public class MonitoringRunningExam implements TemplateComposer {
}; };
} }
private static Function<PageAction, PageAction> showIssueViewAction(
final MonitoringFilter monitoringStatus,
final ClientConnectionTable clientTable,
final ConnectionIssueStatus connectionIssueStatus) {
return action -> {
monitoringStatus.showIssue(connectionIssueStatus);
clientTable.removeSelection();
return action;
};
}
private static Function<PageAction, PageAction> hideIssueViewAction(
final MonitoringFilter monitoringStatus,
final ClientConnectionTable clientTable,
final ConnectionIssueStatus connectionIssueStatus) {
return action -> {
monitoringStatus.hideIssue(connectionIssueStatus);
clientTable.removeSelection();
return action;
};
}
private Set<EntityKey> selectionForInstruction(final ClientConnectionTable clientTable) { private Set<EntityKey> selectionForInstruction(final ClientConnectionTable clientTable) {
final Set<String> connectionTokens = clientTable.getConnectionTokens( final Set<String> connectionTokens = clientTable.getConnectionTokens(
cc -> cc.getStatus().clientActiveStatus, cc -> cc.getStatus().clientActiveStatus,

View file

@ -15,6 +15,7 @@ import java.util.EnumSet;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -26,6 +27,7 @@ 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.ClientGroup;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; 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.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionIssueStatus;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageContext;
@ -47,6 +49,9 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
static final Logger log = LoggerFactory.getLogger(FullPageMonitoringUpdate.class); static final Logger log = LoggerFactory.getLogger(FullPageMonitoringUpdate.class);
private static final String USER_SESSION_STATUS_FILTER_ATTRIBUTE = "USER_SESSION_STATUS_FILTER"; private static final String USER_SESSION_STATUS_FILTER_ATTRIBUTE = "USER_SESSION_STATUS_FILTER";
private static final String USER_SESSION_ISSUE_FILTER_ATTRIBUTE = "USER_SESSION_ISSUE_FILTER";
private static final String USER_SESSION_GROUP_FILTER_ATTRIBUTE = "USER_SESSION_GROUP_FILTER"; private static final String USER_SESSION_GROUP_FILTER_ATTRIBUTE = "USER_SESSION_GROUP_FILTER";
private final ServerPushService serverPushService; private final ServerPushService serverPushService;
@ -59,6 +64,10 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
private final EnumSet<ConnectionStatus> statusFilter; private final EnumSet<ConnectionStatus> statusFilter;
private String statusFilterParam = ""; private String statusFilterParam = "";
private final EnumSet<ConnectionIssueStatus> issueFilter;
private String issueFilterParam = "";
private final Set<Long> clientGroupFilter; private final Set<Long> clientGroupFilter;
private String clientGroupFilterParam = ""; private String clientGroupFilterParam = "";
private boolean filterChanged = false; private boolean filterChanged = false;
@ -83,7 +92,10 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
this.guiUpdates = guiUpdates; this.guiUpdates = guiUpdates;
this.statusFilter = EnumSet.noneOf(ConnectionStatus.class); this.statusFilter = EnumSet.noneOf(ConnectionStatus.class);
loadFilter(); loadStatusFilter();
this.issueFilter = EnumSet.noneOf(ConnectionIssueStatus.class);
loadIssueFilter();
final Collection<ClientGroup> clientGroups = pageService.getRestService() final Collection<ClientGroup> clientGroups = pageService.getRestService()
.getBuilder(GetClientGroups.class) .getBuilder(GetClientGroups.class)
@ -127,6 +139,16 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
return this.statusFilterParam; return this.statusFilterParam;
} }
@Override
public EnumSet<ConnectionIssueStatus> getIssueFilter() {
return this.issueFilter;
}
@Override
public String getIssueFilterParam() {
return this.issueFilterParam;
}
@Override @Override
public boolean filterChanged() { public boolean filterChanged() {
return this.filterChanged; return this.filterChanged;
@ -184,6 +206,18 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
saveFilter(); saveFilter();
} }
@Override
public void hideIssue(final ConnectionIssueStatus connectionIssueStatus) {
this.issueFilter.add(connectionIssueStatus);
saveFilter();
}
@Override
public void showIssue(final ConnectionIssueStatus connectionIssueStatus){
this.issueFilter.remove(connectionIssueStatus);
saveFilter();
}
@Override @Override
public MonitoringFullPageData getMonitoringFullPageData() { public MonitoringFullPageData getMonitoringFullPageData() {
return this.monitoringFullPageData; return this.monitoringFullPageData;
@ -214,9 +248,10 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
} }
private void updateBusinessData() { private void updateBusinessData() {
RestCall<MonitoringFullPageData>.RestCallBuilder restCallBuilder = this.restCallBuilder RestCall<MonitoringFullPageData>.RestCallBuilder restCallBuilder = this.restCallBuilder
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam); .withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam)
.withHeader(API.EXAM_MONITORING_ISSUE_FILTER, this.issueFilterParam);
if (hasClientGroupFilter()) { if (hasClientGroupFilter()) {
restCallBuilder = restCallBuilder restCallBuilder = restCallBuilder
.withHeader(API.EXAM_MONITORING_CLIENT_GROUP_FILTER, this.clientGroupFilterParam); .withHeader(API.EXAM_MONITORING_CLIENT_GROUP_FILTER, this.clientGroupFilterParam);
@ -249,6 +284,11 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
.putAttribute( .putAttribute(
USER_SESSION_STATUS_FILTER_ATTRIBUTE, USER_SESSION_STATUS_FILTER_ATTRIBUTE,
StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR)); StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR));
this.pageService
.getCurrentUser()
.putAttribute(
USER_SESSION_ISSUE_FILTER_ATTRIBUTE,
StringUtils.join(this.issueFilter, Constants.LIST_SEPARATOR));
if (hasClientGroupFilter()) { if (hasClientGroupFilter()) {
this.pageService this.pageService
.getCurrentUser() .getCurrentUser()
@ -260,6 +300,7 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
log.warn("Failed to save status filter to user session"); log.warn("Failed to save status filter to user session");
} finally { } finally {
this.statusFilterParam = StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR); this.statusFilterParam = StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR);
this.issueFilterParam = StringUtils.join(this.issueFilter, Constants.LIST_SEPARATOR);
if (hasClientGroupFilter()) { if (hasClientGroupFilter()) {
this.clientGroupFilterParam = StringUtils.join(this.clientGroupFilter, Constants.LIST_SEPARATOR); this.clientGroupFilterParam = StringUtils.join(this.clientGroupFilter, Constants.LIST_SEPARATOR);
} }
@ -267,7 +308,7 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
} }
} }
private void loadFilter() { private void loadStatusFilter() {
try { try {
final String attribute = this.pageService final String attribute = this.pageService
.getCurrentUser() .getCurrentUser()
@ -306,6 +347,26 @@ public class FullPageMonitoringUpdate implements MonitoringFilter {
} }
} }
private void loadIssueFilter() {
try {
final String attribute = this.pageService
.getCurrentUser()
.getAttribute(USER_SESSION_ISSUE_FILTER_ATTRIBUTE);
this.issueFilter.clear();
if (attribute != null) {
Arrays.asList(StringUtils.split(attribute, Constants.LIST_SEPARATOR))
.forEach(name -> this.issueFilter.add(ConnectionIssueStatus.valueOf(name)));
}
} catch (final Exception e) {
log.warn("Failed to load status filter to user session");
this.issueFilter.clear();
} finally {
this.issueFilterParam = StringUtils.join(this.issueFilter, Constants.LIST_SEPARATOR);
this.filterChanged = true;
}
}
public void recoverFromDisposedRestTemplate(final Exception error) { public void recoverFromDisposedRestTemplate(final Exception error) {
try { try {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {

View file

@ -14,6 +14,7 @@ import java.util.EnumSet;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup; 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.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionIssueStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringData; import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringData;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
@ -26,6 +27,10 @@ public interface MonitoringFilter {
String getStatusFilterParam(); String getStatusFilterParam();
EnumSet<ConnectionIssueStatus> getIssueFilter();
String getIssueFilterParam();
boolean filterChanged(); boolean filterChanged();
void resetFilterChanged(); void resetFilterChanged();
@ -48,6 +53,10 @@ public interface MonitoringFilter {
void showClientGroup(Long clientGroupId); void showClientGroup(Long clientGroupId);
void hideIssue(ConnectionIssueStatus connectionIssueStatus);
void showIssue(ConnectionIssueStatus connectionIssueStatus);
MonitoringFullPageData getMonitoringFullPageData(); MonitoringFullPageData getMonitoringFullPageData();
default MonitoringSEBConnectionData getMonitoringSEBConnectionData() { default MonitoringSEBConnectionData getMonitoringSEBConnectionData() {
@ -87,6 +96,15 @@ public interface MonitoringFilter {
} }
} }
default int getNumOfConnections(final ConnectionIssueStatus connectionIssueStatus) {
final MonitoringSEBConnectionData monitoringSEBConnectionData = getMonitoringSEBConnectionData();
if (monitoringSEBConnectionData != null) {
return monitoringSEBConnectionData.getNumberOfConnection(connectionIssueStatus);
} else {
return 0;
}
}
default Collection<RemoteProctoringRoom> proctoringData() { default Collection<RemoteProctoringRoom> proctoringData() {
final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData(); final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData();
if (monitoringFullPageData != null) { if (monitoringFullPageData != null) {

View file

@ -13,6 +13,7 @@ import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -44,6 +45,7 @@ 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.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionIssueStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringDataView; import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringDataView;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData;
@ -437,6 +439,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
? new HashMap<>() ? new HashMap<>()
: null; : null;
final int[] issueMapping = new int[ConnectionIssueStatus.values().length];
for (int i = 0; i < issueMapping.length; i++) {
issueMapping[i] = 0;
}
updateClientConnections(examId); updateClientConnections(examId);
final List<? extends ClientMonitoringDataView> filteredConnections = this.clientConnectionDAO final List<? extends ClientMonitoringDataView> filteredConnections = this.clientConnectionDAO
@ -448,6 +455,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
.map(c -> { .map(c -> {
statusMapping[c.clientConnection.status.code]++; statusMapping[c.clientConnection.status.code]++;
processClientGroupMapping(c.groups, clientGroupMapping); processClientGroupMapping(c.groups, clientGroupMapping);
processIssueMapping(c.clientConnection, issueMapping);
return c; return c;
}) })
.filter(filter) .filter(filter)
@ -457,6 +465,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
return new MonitoringSEBConnectionData( return new MonitoringSEBConnectionData(
statusMapping, statusMapping,
clientGroupMapping, clientGroupMapping,
issueMapping,
filteredConnections); filteredConnections);
}); });
} }
@ -633,6 +642,20 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}); });
} }
private void processIssueMapping(final ClientConnection clientConnection, final int[] issueMapping){
if (clientConnection == null || issueMapping == null) {
return;
}
if(BooleanUtils.isFalse(clientConnection.securityCheckGranted)){
issueMapping[0] += 1;
}
if(BooleanUtils.isFalse(clientConnection.clientVersionGranted)){
issueMapping[1] += 1;
}
}
private final Map<String, Long> duplicateCheck = new HashMap<>(); private final Map<String, Long> duplicateCheck = new HashMap<>();
private ClientConnectionDataInternal getForTokenAndCheckDuplication( private ClientConnectionDataInternal getForTokenAndCheckDuplication(

View file

@ -22,6 +22,7 @@ import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; import javax.validation.Valid;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -48,6 +49,7 @@ import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionIssueStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
@ -265,14 +267,14 @@ public class ExamMonitoringController {
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, @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( @RequestHeader(name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER, required = false) final String hiddenClientGroups,
name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER, @RequestHeader(name = API.EXAM_MONITORING_ISSUE_FILTER, required = false) final String hiddenIssues){
required = false) final String hiddenClientGroups) {
checkPrivileges(institutionId, examId); checkPrivileges(institutionId, examId);
return this.examSessionService return this.examSessionService
.getConnectionData(examId, createMonitoringFilter(hiddenStates, hiddenClientGroups)) .getConnectionData(examId, createMonitoringFilter(hiddenStates, hiddenClientGroups, hiddenIssues))
.getOrThrow(); .getOrThrow();
} }
@ -315,9 +317,9 @@ public class ExamMonitoringController {
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, @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( @RequestHeader(name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER, required = false) final String hiddenClientGroups,
name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER, @RequestHeader(name = API.EXAM_MONITORING_ISSUE_FILTER, required = false) final String hiddenIssues){
required = false) final String hiddenClientGroups) {
// TODO respond this within another Thread-pool (Executor) // TODO respond this within another Thread-pool (Executor)
// TODO try to cache some MonitoringSEBConnectionData throughout multiple requests (for about 2 sec.) // TODO try to cache some MonitoringSEBConnectionData throughout multiple requests (for about 2 sec.)
@ -328,7 +330,7 @@ public class ExamMonitoringController {
final MonitoringSEBConnectionData monitoringSEBConnectionData = this.examSessionService final MonitoringSEBConnectionData monitoringSEBConnectionData = this.examSessionService
.getMonitoringSEBConnectionsData( .getMonitoringSEBConnectionsData(
examId, examId,
createMonitoringFilter(hiddenStates, hiddenClientGroups)) createMonitoringFilter(hiddenStates, hiddenClientGroups, hiddenIssues))
.getOrThrow(); .getOrThrow();
final boolean proctoringEnabled = this.examAdminService.isProctoringEnabled(runningExam); final boolean proctoringEnabled = this.examAdminService.isProctoringEnabled(runningExam);
@ -544,6 +546,20 @@ public class ExamMonitoringController {
return conn -> conn != null && !filterStates.contains(conn.clientConnection.status); return conn -> conn != null && !filterStates.contains(conn.clientConnection.status);
} }
private Predicate<ClientConnectionData> activeIssueFilterSebVersion(final EnumSet<ConnectionIssueStatus> filterStates) {
if(!filterStates.contains(ConnectionIssueStatus.SEB_VERSION_GRANTED)){
return null;
}
return conn -> conn != null && BooleanUtils.isFalse(conn.clientConnection.clientVersionGranted);
}
private Predicate<ClientConnectionData> activeIssueFilterAsk(final EnumSet<ConnectionIssueStatus> filterStates) {
if(!filterStates.contains(ConnectionIssueStatus.ASK_GRANTED)){
return null;
}
return conn -> conn != null && BooleanUtils.isFalse(conn.clientConnection.securityCheckGranted);
}
/** If we have a filter criteria for ACTIVE connection, we shall filter only the active connections /** If we have a filter criteria for ACTIVE connection, we shall filter only the active connections
* that has no incident. */ * that has no incident. */
private Predicate<ClientConnectionData> withActiveFilter(final EnumSet<ConnectionStatus> filterStates) { private Predicate<ClientConnectionData> withActiveFilter(final EnumSet<ConnectionStatus> filterStates) {
@ -560,7 +576,8 @@ public class ExamMonitoringController {
private Predicate<ClientConnectionData> createMonitoringFilter( private Predicate<ClientConnectionData> createMonitoringFilter(
final String hiddenStates, final String hiddenStates,
final String hiddenClientGroups) { final String hiddenClientGroups,
final String hiddenIssues) {
final EnumSet<ConnectionStatus> filterStates = EnumSet.noneOf(ConnectionStatus.class); final EnumSet<ConnectionStatus> filterStates = EnumSet.noneOf(ConnectionStatus.class);
if (StringUtils.isNotBlank(hiddenStates)) { if (StringUtils.isNotBlank(hiddenStates)) {
@ -581,6 +598,25 @@ public class ExamMonitoringController {
? withActiveFilter(filterStates) ? withActiveFilter(filterStates)
: noneActiveFilter(filterStates); : noneActiveFilter(filterStates);
final EnumSet<ConnectionIssueStatus> filterIssues = EnumSet.noneOf(ConnectionIssueStatus.class);
if (StringUtils.isNotBlank(hiddenIssues)) {
final String[] split = StringUtils.split(hiddenIssues, Constants.LIST_SEPARATOR);
for (final String s : split) {
filterIssues.add(ConnectionIssueStatus.valueOf(s));
}
}
final Predicate<ClientConnectionData> issueFilterSebVersion =
filterIssues.isEmpty()
? null
: activeIssueFilterSebVersion(filterIssues);
final Predicate<ClientConnectionData> issueFilterAsk =
filterIssues.isEmpty()
? null
: activeIssueFilterAsk(filterIssues);
Set<Long> filterClientGroups = null; Set<Long> filterClientGroups = null;
if (StringUtils.isNotBlank(hiddenClientGroups)) { if (StringUtils.isNotBlank(hiddenClientGroups)) {
filterClientGroups = new HashSet<>(); filterClientGroups = new HashSet<>();
@ -595,7 +631,18 @@ public class ExamMonitoringController {
if (ccd == null) { if (ccd == null) {
return false; return false;
} }
return stateFilter.test(ccd) && ccd.filter(_filterClientGroups);
boolean result = stateFilter.test(ccd) && ccd.filter(_filterClientGroups);
if (issueFilterSebVersion != null) {
result = result && issueFilterSebVersion.test(ccd);
}
if (issueFilterAsk != null) {
result = result && issueFilterAsk.test(ccd);
}
return result;
}; };
} }

View file

@ -2210,6 +2210,7 @@ sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} )
sebserver.monitoring.exam.action.viewgroup=View {0} ( {1} ) sebserver.monitoring.exam.action.viewgroup=View {0} ( {1} )
sebserver.exam.monitoring.action.category.statefilter=State Filter sebserver.exam.monitoring.action.category.statefilter=State Filter
sebserver.exam.monitoring.action.category.groupfilter=Client Group Filter sebserver.exam.monitoring.action.category.groupfilter=Client Group Filter
sebserver.exam.monitoring.action.category.issuefilter=Issue Filter
sebserver.exam.overall.action.category.proctoring=Live Proctoring sebserver.exam.overall.action.category.proctoring=Live Proctoring
sebserver.monitoring.exam.action.proctoring.openTownhall=Open Townhall sebserver.monitoring.exam.action.proctoring.openTownhall=Open Townhall
sebserver.monitoring.exam.action.proctoring.showTownhall=Show Townhall sebserver.monitoring.exam.action.proctoring.showTownhall=Show Townhall
@ -2301,6 +2302,10 @@ 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.show.undefined=Show Undefined ( {0} )
sebserver.monitoring.exam.connection.action.hide.clientgroup=Hide {0} ( {1} ) 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.show.clientgroup=Show {0} ( {1} )
sebserver.monitoring.exam.connection.action.hide.askgranted=Hide Ask Granted ( {0} )
sebserver.monitoring.exam.connection.action.show.askgranted=Show Ask Granted ( {0} )
sebserver.monitoring.exam.connection.action.hide.sebversiongranted=Hide SEB Version Granted ( {0} )
sebserver.monitoring.exam.connection.action.show.sebversiongranted=Show SEB Version Granted ( {0} )
sebserver.monitoring.exam.connection.action.proctoring=Single Room Proctoring sebserver.monitoring.exam.connection.action.proctoring=Single Room Proctoring
sebserver.monitoring.exam.connection.action.proctoring.examroom=Exam 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.<br/>Are you sure to open the town-hall? 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.<br/>Are you sure to open the town-hall?