SEBSERV-188 implementation

This commit is contained in:
anhefti 2022-01-18 13:33:23 +01:00
parent fc398d3ff9
commit b1b582028f
24 changed files with 1033 additions and 521 deletions

View file

@ -188,6 +188,7 @@ public final class API {
public static final String USER_ACTIVITY_LOG_ENDPOINT = "/useractivity";
public static final String EXAM_MONITORING_ENDPOINT = "/monitoring";
public static final String EXAM_MONITORING_FULLPAGE = "/fullpage";
public static final String EXAM_MONITORING_INSTRUCTION_ENDPOINT = "/instruction";
public static final String EXAM_MONITORING_NOTIFICATION_ENDPOINT = "/notification";
public static final String EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT = "/disable-connection";

View file

@ -26,23 +26,24 @@ import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
public final class ClientConnection implements GrantEntity {
public enum ConnectionStatus {
UNDEFINED(false, false),
CONNECTION_REQUESTED(true, false),
AUTHENTICATED(true, true),
ACTIVE(false, true),
CLOSED(false, false),
DISABLED(false, false);
UNDEFINED(0, false, false),
CONNECTION_REQUESTED(1, true, false),
AUTHENTICATED(2, true, true),
ACTIVE(3, false, true),
CLOSED(4, false, false),
DISABLED(5, false, false);
public final int code;
public final boolean connectingStatus;
public final boolean establishedStatus;
public final boolean clientActiveStatus;
ConnectionStatus(final boolean connectingStatus, final boolean establishedStatus) {
ConnectionStatus(final int code, final boolean connectingStatus, final boolean establishedStatus) {
this.code = code;
this.connectingStatus = connectingStatus;
this.establishedStatus = establishedStatus;
this.clientActiveStatus = connectingStatus || establishedStatus;
}
}
public static final ClientConnection EMPTY_CLIENT_CONNECTION = new ClientConnection(

View file

@ -0,0 +1,91 @@
/*
* 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.gbl.model.session;
import java.util.Collection;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.model.Domain;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MonitoringFullPageData {
public static final String ATTR_CONNECTIONS_DATA = "monitoringConnectionData";
public static final String ATTR_PROCTORING_DATA = "proctoringData";
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID)
public final Long examId;
@JsonProperty(ATTR_CONNECTIONS_DATA)
public final MonitoringSEBConnectionData monitoringConnectionData;
@JsonProperty(ATTR_PROCTORING_DATA)
public final Collection<RemoteProctoringRoom> proctoringData;
public MonitoringFullPageData(
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId,
@JsonProperty(ATTR_CONNECTIONS_DATA) final MonitoringSEBConnectionData monitoringConnectionData,
@JsonProperty(ATTR_PROCTORING_DATA) final Collection<RemoteProctoringRoom> proctoringData) {
this.examId = examId;
this.monitoringConnectionData = monitoringConnectionData;
this.proctoringData = proctoringData;
}
public Long getExamId() {
return this.examId;
}
public MonitoringSEBConnectionData getMonitoringConnectionData() {
return this.monitoringConnectionData;
}
public Collection<RemoteProctoringRoom> getProctoringData() {
return this.proctoringData;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.examId == null) ? 0 : this.examId.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final MonitoringFullPageData other = (MonitoringFullPageData) obj;
if (this.examId == null) {
if (other.examId != null)
return false;
} else if (!this.examId.equals(other.examId))
return false;
return true;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("OverallMonitroingData [examId=");
builder.append(this.examId);
builder.append(", monitoringConnectionData=");
builder.append(this.monitoringConnectionData);
builder.append(", proctoringData=");
builder.append(this.proctoringData);
builder.append("]");
return builder.toString();
}
}

View file

@ -0,0 +1,104 @@
/*
* 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.gbl.model.session;
import java.util.Arrays;
import java.util.Collection;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MonitoringSEBConnectionData {
public static final String ATTR_CONNECTIONS = "connections";
public static final String ATTR_STATUS_MAPPING = "statusMapping";
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID)
public final Long examId;
@JsonProperty(ATTR_CONNECTIONS)
public final Collection<ClientConnectionData> connections;
@JsonProperty(ATTR_STATUS_MAPPING)
public final int[] connectionsPerStatus;
@JsonCreator
public MonitoringSEBConnectionData(
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId,
@JsonProperty(ATTR_CONNECTIONS) final Collection<ClientConnectionData> connections,
@JsonProperty(ATTR_STATUS_MAPPING) final int[] connectionsPerStatus) {
this.examId = examId;
this.connections = connections;
this.connectionsPerStatus = connectionsPerStatus;
}
public Long getExamId() {
return this.examId;
}
public Collection<ClientConnectionData> getConnections() {
return this.connections;
}
public int[] getConnectionsPerStatus() {
return this.connectionsPerStatus;
}
@JsonIgnore
public int getNumberOfConnection(final ConnectionStatus status) {
if (this.connectionsPerStatus == null || this.connectionsPerStatus.length <= status.code) {
return -1;
}
return this.connectionsPerStatus[status.code];
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.examId == null) ? 0 : this.examId.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final MonitoringSEBConnectionData other = (MonitoringSEBConnectionData) obj;
if (this.examId == null) {
if (other.examId != null)
return false;
} else if (!this.examId.equals(other.examId))
return false;
return true;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("MonitoringSEBConnectionData [examId=");
builder.append(this.examId);
builder.append(", connections=");
builder.append(this.connections);
builder.append(", connectionsPerStatus=");
builder.append(Arrays.toString(this.connectionsPerStatus));
builder.append("]");
return builder.toString();
}
}

View file

@ -38,7 +38,6 @@ import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.content.monitoring.MonitoringRunningExam.ProctoringUpdateErrorHandler;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
@ -49,6 +48,7 @@ import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
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.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
@ -275,14 +275,14 @@ public class MonitoringClientConnection implements TemplateComposer {
final Supplier<EntityTable<ClientNotification>> notificationTableSupplier = _notificationTableSupplier;
// server push update
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
final UpdateErrorHandler updateErrorHandler =
new UpdateErrorHandler(this.pageService, pageContext);
this.serverPushService.runServerPush(
new ServerPushContext(
content,
Utils.truePredicate(),
proctoringUpdateErrorHandler),
updateErrorHandler),
this.pollInterval,
context -> clientConnectionDetails.updateData(),
context -> clientConnectionDetails.updateGUI(notificationTableSupplier, pageContext));

View file

@ -8,20 +8,18 @@
package ch.ethz.seb.sebserver.gui.content.monitoring;
import java.util.ArrayList;
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;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.MessageBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.eclipse.swt.widgets.TreeItem;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@ -36,16 +34,16 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
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.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.GuiServiceInfo;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.content.action.ActionPane;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageMessageException;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
@ -53,27 +51,26 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
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.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
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.proctoring.MonitoringProctoringService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.widget.Message;
@Lazy
@Component
@GuiProfile
public class MonitoringRunningExam implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(MonitoringRunningExam.class);
//private static final Logger log = LoggerFactory.getLogger(MonitoringRunningExam.class);
private static final LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.emptySelection");
@ -100,7 +97,6 @@ public class MonitoringRunningExam implements TemplateComposer {
private final MonitoringProctoringService monitoringProctoringService;
private final boolean distributedSetup;
private final long pollInterval;
private final long proctoringRoomUpdateInterval;
protected MonitoringRunningExam(
final ServerPushService serverPushService,
@ -110,8 +106,7 @@ public class MonitoringRunningExam implements TemplateComposer {
final MonitoringExamSearchPopup monitoringExamSearchPopup,
final MonitoringProctoringService monitoringProctoringService,
final GuiServiceInfo guiServiceInfo,
@Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval,
@Value("${sebserver.gui.remote.proctoring.rooms.update.poll-interval:5000}") final long proctoringRoomUpdateInterval) {
@Value("${sebserver.gui.webservice.poll-interval:2000}") final long pollInterval) {
this.serverPushService = serverPushService;
this.pageService = pageService;
@ -123,7 +118,6 @@ public class MonitoringRunningExam implements TemplateComposer {
this.pollInterval = pollInterval;
this.distributedSetup = guiServiceInfo.isDistributedSetup();
this.monitoringExamSearchPopup = monitoringExamSearchPopup;
this.proctoringRoomUpdateInterval = proctoringRoomUpdateInterval;
}
@Override
@ -158,27 +152,21 @@ public class MonitoringRunningExam implements TemplateComposer {
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCall =
restService.getBuilder(GetClientConnectionDataList.class)
.withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId());
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
final ServerPushContext pushContext = new ServerPushContext(
content,
Utils.truePredicate(),
proctoringUpdateErrorHandler);
final Collection<FullPageMonitoringGUIUpdate> guiUpdates = new ArrayList<>();
final FullPageMonitoringUpdate fullPageMonitoringUpdate = new FullPageMonitoringUpdate(
exam.id,
this.pageService,
this.serverPushService,
this.asyncRunner,
guiUpdates);
final ClientConnectionTable clientTable = new ClientConnectionTable(
this.pageService,
tablePane,
this.asyncRunner,
exam,
indicators,
restCall,
pushContext,
this.distributedSetup);
guiUpdates.add(clientTable);
clientTable
.withDefaultAction(
@ -194,12 +182,6 @@ public class MonitoringRunningExam implements TemplateComposer {
ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION,
ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM));
this.serverPushService.runServerPush(
pushContext,
this.pollInterval,
context -> clientTable.updateValues(),
updateTableGUI(clientTable));
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
@ -256,262 +238,199 @@ public class MonitoringRunningExam implements TemplateComposer {
.publishIf(isExamSupporter, false);
if (isExamSupporter.getAsBoolean()) {
addFilterActions(actionBuilder, clientTable, isExamSupporter);
addProctoringActions(
currentUser.getProctoringGUIService(),
pageContext,
content,
actionBuilder);
guiUpdates.add(createFilterActions(
fullPageMonitoringUpdate,
actionBuilder,
clientTable,
isExamSupporter));
final ProctoringServiceSettings proctoringSettings = this.restService
.getBuilder(GetProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.getOr(null);
if (proctoringSettings != null && proctoringSettings.enableProctoring) {
guiUpdates.add(createProctoringActions(
proctoringSettings,
currentUser.getProctoringGUIService(),
pageContext,
content,
actionBuilder));
}
}
// finally start the page update (server push)
fullPageMonitoringUpdate.start(pageContext, content, this.pollInterval);
}
private void addProctoringActions(
private FullPageMonitoringGUIUpdate createProctoringActions(
final ProctoringServiceSettings proctoringSettings,
final ProctoringGUIService proctoringGUIService,
final PageContext pageContext,
final Composite parent,
final PageActionBuilder actionBuilder) {
final EntityKey entityKey = pageContext.getEntityKey();
final ProctoringServiceSettings proctoringSettings = this.restService
.getBuilder(GetProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.getOr(null);
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) {
final EntityKey entityKey = pageContext.getEntityKey();
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withConfirm(action -> {
if (!this.monitoringProctoringService.isTownhallRoomActive(action.getEntityKey().modelId)) {
return CONFIRM_OPEN_TOWNHALL;
} else {
return CONFIRM_CLOSE_TOWNHALL;
}
})
.withExec(action -> this.monitoringProctoringService.toggleTownhallRoom(proctoringGUIService,
action))
.noEventPropagation()
.publish();
if (proctoringSettings != null && proctoringSettings.enableProctoring) {
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withConfirm(action -> {
if (!this.monitoringProctoringService.isTownhallRoomActive(action.getEntityKey().modelId)) {
return CONFIRM_OPEN_TOWNHALL;
} else {
return CONFIRM_CLOSE_TOWNHALL;
}
})
.withExec(action -> this.monitoringProctoringService.toggleTownhallRoom(proctoringGUIService,
action))
.noEventPropagation()
.publish();
if (this.monitoringProctoringService.isTownhallRoomActive(entityKey.modelId)) {
this.pageService.firePageEvent(
new ActionActivationEvent(
true,
new Tuple<>(
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM,
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)),
pageContext);
}
if (this.monitoringProctoringService.isTownhallRoomActive(entityKey.modelId)) {
this.pageService.firePageEvent(
new ActionActivationEvent(
true,
new Tuple<>(
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM,
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)),
pageContext);
}
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
final ServerPushContext pushContext = new ServerPushContext(
parent,
Utils.truePredicate(),
proctoringUpdateErrorHandler);
this.monitoringProctoringService.initCollectingRoomActions(
pushContext,
pageContext,
actionBuilder,
proctoringSettings,
proctoringGUIService);
this.serverPushService.runServerPush(
pushContext,
this.proctoringRoomUpdateInterval,
context -> this.monitoringProctoringService.updateCollectingRoomActions(
context,
pageContext,
actionBuilder,
proctoringSettings,
proctoringGUIService));
}
this.monitoringProctoringService.initCollectingRoomActions(
pageContext,
actionBuilder,
proctoringSettings,
proctoringGUIService);
return monitoringStatus -> this.monitoringProctoringService.updateCollectingRoomActions(
monitoringStatus.proctoringData(),
pageContext,
actionBuilder,
proctoringSettings,
proctoringGUIService);
}
private void addFilterActions(
private FullPageMonitoringGUIUpdate createFilterActions(
final MonitoringStatus monitoringStatus,
final PageActionBuilder actionBuilder,
final ClientConnectionTable clientTable,
final BooleanSupplier isExamSupporter) {
final StatusFilterGUIUpdate statusFilterGUIUpdate =
new StatusFilterGUIUpdate(this.pageService.getPolyglotPageService());
addFilterAction(
monitoringStatus,
statusFilterGUIUpdate,
actionBuilder,
clientTable,
ConnectionStatus.CONNECTION_REQUESTED,
ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION,
ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION);
addFilterAction(
monitoringStatus,
statusFilterGUIUpdate,
actionBuilder,
clientTable,
ConnectionStatus.ACTIVE,
ActionDefinition.MONITOR_EXAM_SHOW_ACTIVE_CONNECTION,
ActionDefinition.MONITOR_EXAM_HIDE_ACTIVE_CONNECTION);
addFilterAction(
monitoringStatus,
statusFilterGUIUpdate,
actionBuilder,
clientTable,
ConnectionStatus.CLOSED,
ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION,
ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION);
addFilterAction(
monitoringStatus,
statusFilterGUIUpdate,
actionBuilder,
clientTable,
ConnectionStatus.DISABLED,
ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION,
ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION);
// addRequestedFilterAction(actionBuilder, clientTable);
// addActiveFilterAction(actionBuilder, clientTable);
// addClosedFilterAction(actionBuilder, clientTable);
// addDisabledFilterAction(actionBuilder, clientTable);
return statusFilterGUIUpdate;
}
// private void addRequestedFilterAction(
// final PageActionBuilder actionBuilder,
// final ClientConnectionTable clientTable) {
//
// if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) {
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION)
// .withExec(showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
// .noEventPropagation()
// .withSwitchAction(
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION)
// .withExec(
// hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
// .noEventPropagation()
// .create())
// .publish();
// } else {
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION)
// .withExec(hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
// .noEventPropagation()
// .withSwitchAction(
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION)
// .withExec(
// showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED))
// .noEventPropagation()
// .create())
// .publish();
// }
// }
private void addFilterAction(
final MonitoringStatus monitoringStatus,
final StatusFilterGUIUpdate statusFilterGUIUpdate,
final PageActionBuilder actionBuilder,
final ClientConnectionTable clientTable,
final ConnectionStatus status,
final ActionDefinition showAction,
final ActionDefinition hideAction) {
final ActionDefinition showActionDef,
final ActionDefinition hideActionDef) {
if (clientTable.isStatusHidden(status)) {
actionBuilder.newAction(showAction)
.withExec(showStateViewAction(clientTable, status))
final int numOfConnections = monitoringStatus.getNumOfConnections(status);
if (monitoringStatus.isStatusHidden(status)) {
final PageAction showAction = actionBuilder.newAction(showActionDef)
.withExec(showStateViewAction(monitoringStatus, clientTable, status))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(hideAction)
actionBuilder.newAction(hideActionDef)
.withExec(
hideStateViewAction(clientTable, status))
hideStateViewAction(monitoringStatus, clientTable, status))
.noEventPropagation()
.withNameAttributes(numOfConnections)
.create())
.publish();
.withNameAttributes(numOfConnections)
.create();
this.pageService.publishAction(
showAction,
treeItem -> statusFilterGUIUpdate.register(treeItem, status));
} else {
actionBuilder.newAction(hideAction)
.withExec(hideStateViewAction(clientTable, status))
final PageAction hideAction = actionBuilder.newAction(hideActionDef)
.withExec(hideStateViewAction(monitoringStatus, clientTable, status))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(showAction)
actionBuilder.newAction(showActionDef)
.withExec(
showStateViewAction(clientTable, status))
showStateViewAction(monitoringStatus, clientTable, status))
.noEventPropagation()
.withNameAttributes(numOfConnections)
.create())
.publish();
.withNameAttributes(numOfConnections)
.create();
this.pageService.publishAction(
hideAction,
treeItem -> statusFilterGUIUpdate.register(treeItem, status));
}
}
// private void addActiveFilterAction(
// final PageActionBuilder actionBuilder,
// final ClientConnectionTable clientTable) {
//
// if (clientTable.isStatusHidden(ConnectionStatus.ACTIVE)) {
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_ACTIVE_CONNECTION)
// .withExec(showStateViewAction(clientTable, ConnectionStatus.ACTIVE))
// .noEventPropagation()
// .withSwitchAction(
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_ACTIVE_CONNECTION)
// .withExec(
// hideStateViewAction(clientTable, ConnectionStatus.ACTIVE))
// .noEventPropagation()
// .create())
// .publish();
// } else {
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_ACTIVE_CONNECTION)
// .withExec(hideStateViewAction(clientTable, ConnectionStatus.ACTIVE))
// .noEventPropagation()
// .withSwitchAction(
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_ACTIVE_CONNECTION)
// .withExec(
// showStateViewAction(clientTable, ConnectionStatus.ACTIVE))
// .noEventPropagation()
// .create())
// .publish();
// }
// }
//
// private void addDisabledFilterAction(
// final PageActionBuilder actionBuilder,
// final ClientConnectionTable clientTable) {
//
// if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) {
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION)
// .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED))
// .noEventPropagation()
// .withSwitchAction(
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION)
// .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED))
// .noEventPropagation()
// .create())
// .publish();
// } else {
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION)
// .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED))
// .noEventPropagation()
// .withSwitchAction(
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION)
// .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED))
// .noEventPropagation()
// .create())
// .publish();
// }
// }
//
// private void addClosedFilterAction(
// final PageActionBuilder actionBuilder,
// final ClientConnectionTable clientTable) {
//
// if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) {
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION)
// .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED))
// .noEventPropagation()
// .withSwitchAction(
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION)
// .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED))
// .noEventPropagation()
// .create())
// .publish();
// } else {
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION)
// .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED))
// .noEventPropagation()
// .withSwitchAction(
// actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION)
// .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED))
// .noEventPropagation()
// .create())
// .publish();
// }
// }
/** This holds the filter action items and implements the specific GUI update for it */
private class StatusFilterGUIUpdate implements FullPageMonitoringGUIUpdate {
private final PolyglotPageService polyglotPageService;
private final TreeItem[] actionItemPerStateFilter = new TreeItem[ConnectionStatus.values().length];
public StatusFilterGUIUpdate(final PolyglotPageService polyglotPageService) {
this.polyglotPageService = polyglotPageService;
}
void register(final TreeItem item, final ConnectionStatus status) {
this.actionItemPerStateFilter[status.code] = item;
}
@Override
public void update(final MonitoringStatus monitoringStatus) {
final ConnectionStatus[] states = ConnectionStatus.values();
for (int i = 0; i < states.length; i++) {
final ConnectionStatus state = states[i];
final int numOfConnections = monitoringStatus.getNumOfConnections(state);
if (numOfConnections >= 0 && this.actionItemPerStateFilter[state.code] != null) {
final TreeItem treeItem = this.actionItemPerStateFilter[state.code];
final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY);
action.setTitleArgument(0, numOfConnections);
this.polyglotPageService.injectI18n(treeItem, action.getTitle());
}
}
}
}
private PageAction openSearchPopup(final PageAction action) {
this.monitoringExamSearchPopup.show(action.pageContext());
@ -519,22 +438,24 @@ public class MonitoringRunningExam implements TemplateComposer {
}
private static Function<PageAction, PageAction> showStateViewAction(
final MonitoringStatus monitoringStatus,
final ClientConnectionTable clientTable,
final ConnectionStatus status) {
return action -> {
clientTable.showStatus(status);
monitoringStatus.showStatus(status);
clientTable.removeSelection();
return action;
};
}
private static Function<PageAction, PageAction> hideStateViewAction(
final MonitoringStatus monitoringStatus,
final ClientConnectionTable clientTable,
final ConnectionStatus status) {
return action -> {
clientTable.hideStatus(status);
monitoringStatus.hideStatus(status);
clientTable.removeSelection();
return action;
};
@ -585,68 +506,4 @@ public class MonitoringRunningExam implements TemplateComposer {
return action;
}
private Consumer<ServerPushContext> updateTableGUI(final ClientConnectionTable clientTable) {
return context -> {
if (!context.isDisposed()) {
try {
clientTable.updateGUI();
context.layout();
} catch (final Exception e) {
if (log.isWarnEnabled()) {
log.warn("Unexpected error while trying to update GUI: ", e);
}
}
}
};
}
static final class ProctoringUpdateErrorHandler implements Function<Exception, Boolean> {
private final PageService pageService;
private final PageContext pageContext;
private int errors = 0;
public ProctoringUpdateErrorHandler(
final PageService pageService,
final PageContext pageContext) {
this.pageService = pageService;
this.pageContext = pageContext;
}
private boolean checkUserSession() {
try {
this.pageService.getCurrentUser().get();
return true;
} catch (final Exception e) {
try {
this.pageContext.forwardToLoginPage();
final MessageBox logoutSuccess = new Message(
this.pageContext.getShell(),
this.pageService.getI18nSupport().getText("sebserver.logout"),
Utils.formatLineBreaks(
this.pageService.getI18nSupport()
.getText("sebserver.logout.invalid-session.message")),
SWT.ICON_INFORMATION,
this.pageService.getI18nSupport());
logoutSuccess.open(null);
} catch (final Exception ee) {
log.warn("Unable to auto-logout: ", ee.getMessage());
}
return true;
}
}
@Override
public Boolean apply(final Exception error) {
this.errors++;
log.error("Failed to update server push: {}", error.getMessage());
if (this.errors > 5) {
checkUserSession();
}
return this.errors > 5;
}
}
}

View file

@ -70,10 +70,10 @@ public final class PageAction {
this.fireActionEvent = fireActionEvent;
this.ignoreMoveAwayFromEdit = ignoreMoveAwayFromEdit;
this.switchAction = switchAction;
this.titleArgs = titleArgs;
if (this.switchAction != null) {
this.switchAction.switchAction = this;
}
this.titleArgs = titleArgs;
if (this.pageContext != null) {
this.pageContext = pageContext.withAttribute(AttributeKeys.READ_ONLY, Constants.TRUE_STRING);
@ -102,6 +102,14 @@ public final class PageAction {
}
}
public void setTitleArgument(final int argIndex, final Object value) {
if (this.titleArgs == null || this.titleArgs.length <= argIndex) {
return;
}
this.titleArgs[argIndex] = value;
}
public PageAction getSwitchAction() {
return this.switchAction;
}

View file

@ -0,0 +1,72 @@
/*
* 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.service.push;
import java.util.function.Function;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.MessageBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.PageService;
import ch.ethz.seb.sebserver.gui.widget.Message;
public final class UpdateErrorHandler implements Function<Exception, Boolean> {
private static final Logger log = LoggerFactory.getLogger(UpdateErrorHandler.class);
private final PageService pageService;
private final PageContext pageContext;
private int errors = 0;
public UpdateErrorHandler(
final PageService pageService,
final PageContext pageContext) {
this.pageService = pageService;
this.pageContext = pageContext;
}
private boolean checkUserSession() {
try {
this.pageService.getCurrentUser().get();
return true;
} catch (final Exception e) {
try {
this.pageContext.forwardToLoginPage();
final MessageBox logoutSuccess = new Message(
this.pageContext.getShell(),
this.pageService.getI18nSupport().getText("sebserver.logout"),
Utils.formatLineBreaks(
this.pageService.getI18nSupport()
.getText("sebserver.logout.invalid-session.message")),
SWT.ICON_INFORMATION,
this.pageService.getI18nSupport());
logoutSuccess.open(null);
} catch (final Exception ee) {
log.warn("Unable to auto-logout: ", ee.getMessage());
}
return true;
}
}
@Override
public Boolean apply(final Exception error) {
this.errors++;
log.error("Failed to update server push: {}", error.getMessage());
if (this.errors > 5) {
checkUserSession();
}
return this.errors > 5;
}
}

View file

@ -0,0 +1,41 @@
/*
* 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.service.remote.webservice.api.session;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.session.MonitoringFullPageData;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetMonitoringFullPageData extends RestCall<MonitoringFullPageData> {
public GetMonitoringFullPageData() {
super(new TypeKey<>(
CallType.GET_SINGLE,
null,
new TypeReference<MonitoringFullPageData>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT
+ API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_MONITORING_FULLPAGE);
}
}

View file

@ -9,11 +9,9 @@
package ch.ethz.seb.sebserver.gui.service.session;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
@ -39,15 +37,11 @@ import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.async.AsyncRunner;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
@ -61,21 +55,15 @@ import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2RestTemplateException;
import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
public final class ClientConnectionTable {
private static final Logger log = LoggerFactory.getLogger(ClientConnectionTable.class);
public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate {
private static final int[] TABLE_PROPORTIONS = new int[] { 3, 3, 2, 1 };
private static final int BOTTOM_PADDING = 20;
private static final int NUMBER_OF_NONE_INDICATOR_COLUMNS = 3;
private static final String USER_SESSION_STATUS_FILTER_ATTRIBUTE = "USER_SESSION_STATUS_FILTER_ATTRIBUTE";
private static final String INDICATOR_NAME_TEXT_KEY_PREFIX =
"sebserver.exam.indicator.type.description.";
@ -93,19 +81,13 @@ public final class ClientConnectionTable {
new LocTextKey("sebserver.monitoring.connection.list.column.status" + Constants.TOOLTIP_TEXT_KEY_SUFFIX);
private final PageService pageService;
private final AsyncRunner asyncRunner;
private final Exam exam;
private final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder;
private final ServerPushContext pushConext;
private final boolean distributedSetup;
private final Map<Long, IndicatorData> indicatorMapping;
private final Table table;
private final ColorData colorData;
private final Function<ClientConnectionData, String> localizedClientConnectionStatusNameFunction;
private final EnumSet<ConnectionStatus> statusFilter;
private String statusFilterParam = "";
private boolean statusFilterChanged = false;
private Consumer<Set<EntityKey>> selectionListener;
private int tableWidth;
@ -118,23 +100,16 @@ public final class ClientConnectionTable {
private final Color lightFontColor;
private boolean forceUpdateAll = false;
private boolean updateInProgress = false;
public ClientConnectionTable(
final PageService pageService,
final Composite tableRoot,
final AsyncRunner asyncRunner,
final Exam exam,
final Collection<Indicator> indicators,
final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder,
final ServerPushContext pushConext,
final boolean distributedSetup) {
this.pageService = pageService;
this.asyncRunner = asyncRunner;
this.exam = exam;
this.restCallBuilder = restCallBuilder;
this.pushConext = pushConext;
this.distributedSetup = distributedSetup;
final WidgetFactory widgetFactory = pageService.getWidgetFactory();
@ -154,8 +129,6 @@ public final class ClientConnectionTable {
this.localizedClientConnectionStatusNameFunction =
resourceService.localizedClientConnectionStatusNameFunction();
this.statusFilter = EnumSet.noneOf(ConnectionStatus.class);
loadStatusFilter();
this.table = widgetFactory.tableLocalized(tableRoot, SWT.MULTI | SWT.V_SCROLL);
final GridLayout gridLayout = new GridLayout(3 + indicators.size(), false);
@ -208,20 +181,6 @@ public final class ClientConnectionTable {
return this.exam;
}
public boolean isStatusHidden(final ConnectionStatus status) {
return this.statusFilter.contains(status);
}
public void hideStatus(final ConnectionStatus status) {
this.statusFilter.add(status);
saveStatusFilter();
}
public void showStatus(final ConnectionStatus status) {
this.statusFilter.remove(status);
saveStatusFilter();
}
public ClientConnectionTable withDefaultAction(final PageAction pageAction, final PageService pageService) {
this.table.addListener(SWT.MouseDoubleClick, event -> {
final Tuple<String> selection = getSingleSelection();
@ -323,64 +282,48 @@ public final class ClientConnectionTable {
this.forceUpdateAll = true;
}
public void updateValues() {
if (this.updateInProgress) {
return;
@Override
public void update(final MonitoringStatus monitoringStatus) {
final boolean needsSync = monitoringStatus.statusFilterChanged() ||
this.forceUpdateAll ||
(this.tableMapping != null &&
this.table != null &&
this.tableMapping.size() != this.table.getItemCount())
||
this.distributedSetup;
if (needsSync) {
this.toDelete.clear();
this.toDelete.addAll(this.tableMapping.keySet());
}
this.updateInProgress = true;
final boolean needsSync = this.tableMapping != null &&
this.table != null &&
this.tableMapping.size() != this.table.getItemCount();
this.asyncRunner.runAsync(() -> updateValuesAsync(needsSync));
}
private void updateValuesAsync(final boolean needsSync) {
try {
final boolean sync = this.statusFilterChanged || this.forceUpdateAll || needsSync || this.distributedSetup;
if (sync) {
this.toDelete.clear();
this.toDelete.addAll(this.tableMapping.keySet());
}
this.restCallBuilder
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam)
.call()
.get(error -> {
recoverFromDisposedRestTemplate(error);
this.pushConext.reportError(error);
return Collections.emptyList();
})
.forEach(data -> {
final UpdatableTableItem tableItem = this.tableMapping.computeIfAbsent(
data.getConnectionId(),
UpdatableTableItem::new);
tableItem.push(data);
if (sync) {
this.toDelete.remove(data.getConnectionId());
}
});
if (!this.toDelete.isEmpty()) {
this.toDelete.forEach(id -> {
final UpdatableTableItem item = this.tableMapping.remove(id);
if (item != null) {
final List<Long> list = this.sessionIds.get(item.connectionData.clientConnection.userSessionId);
if (list != null) {
list.remove(id);
}
monitoringStatus.getConnectionData()
.forEach(data -> {
final UpdatableTableItem tableItem = this.tableMapping.computeIfAbsent(
data.getConnectionId(),
UpdatableTableItem::new);
tableItem.push(data);
if (needsSync) {
this.toDelete.remove(data.getConnectionId());
}
});
this.statusFilterChanged = false;
this.toDelete.clear();
}
this.forceUpdateAll = false;
this.updateInProgress = false;
} catch (final Exception e) {
this.pushConext.reportError(e);
if (!this.toDelete.isEmpty()) {
this.toDelete.forEach(id -> {
final UpdatableTableItem item = this.tableMapping.remove(id);
if (item != null) {
final List<Long> list = this.sessionIds.get(item.connectionData.clientConnection.userSessionId);
if (list != null) {
list.remove(id);
}
}
});
monitoringStatus.resetStatusFilterChanged();
this.toDelete.clear();
}
this.forceUpdateAll = false;
updateGUI();
}
public void updateGUI() {
@ -398,8 +341,7 @@ public final class ClientConnectionTable {
this.needsSort = false;
adaptTableWidth();
this.table.layout(true, true);
this.table.getParent().layout(true, true);
}
private void adaptTableWidth() {
@ -452,44 +394,6 @@ public final class ClientConnectionTable {
(e1, e2) -> e1, LinkedHashMap::new));
}
private void saveStatusFilter() {
try {
this.pageService
.getCurrentUser()
.putAttribute(
USER_SESSION_STATUS_FILTER_ATTRIBUTE,
StringUtils.join(this.statusFilter, 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;
}
}
private void loadStatusFilter() {
try {
final String attribute = this.pageService
.getCurrentUser()
.getAttribute(USER_SESSION_STATUS_FILTER_ATTRIBUTE);
this.statusFilter.clear();
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);
}
} catch (final Exception e) {
log.warn("Failed to load status filter to user session");
this.statusFilter.clear();
this.statusFilter.add(ConnectionStatus.DISABLED);
} finally {
this.statusFilterParam = StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR);
this.statusFilterChanged = true;
}
}
private void notifySelectionChange() {
if (this.selectionListener == null) {
return;
@ -737,13 +641,4 @@ public final class ClientConnectionTable {
}
public void recoverFromDisposedRestTemplate(final Exception error) {
if (log.isDebugEnabled()) {
log.debug("Try to recover from disposed OAuth2 rest template...");
}
if (error instanceof DisposedOAuth2RestTemplateException) {
this.pageService.getRestService().injectCurrentRestTemplate(this.restCallBuilder);
}
}
}

View file

@ -0,0 +1,16 @@
/*
* 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.service.session;
@FunctionalInterface
public interface FullPageMonitoringGUIUpdate {
void update(MonitoringStatus monitoringStatus);
}

View file

@ -0,0 +1,231 @@
/*
* 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.service.session;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
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.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.MonitoringFullPageData;
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.PageService;
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.session.GetMonitoringFullPageData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2RestTemplateException;
/** Encapsulates the update and the current status of all monitoring data needed for a
* 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 {
static final Logger log = LoggerFactory.getLogger(FullPageMonitoringUpdate.class);
private static final String USER_SESSION_STATUS_FILTER_ATTRIBUTE = "USER_SESSION_STATUS_FILTER_ATTRIBUTE";
private final ServerPushService serverPushService;
private final PageService pageService;
private final AsyncRunner asyncRunner;
private final RestCall<MonitoringFullPageData>.RestCallBuilder restCallBuilder;
private final Collection<FullPageMonitoringGUIUpdate> guiUpdates;
private ServerPushContext pushContext;
private final EnumSet<ConnectionStatus> statusFilter;
private String statusFilterParam = "";
private boolean statusFilterChanged = false;
private boolean updateInProgress = false;
private MonitoringFullPageData monitoringFullPageData = null;
public FullPageMonitoringUpdate(
final Long examId,
final PageService pageService,
final ServerPushService serverPushService,
final AsyncRunner asyncRunner,
final Collection<FullPageMonitoringGUIUpdate> guiUpdates) {
this.serverPushService = serverPushService;
this.pageService = pageService;
this.asyncRunner = asyncRunner;
this.restCallBuilder = pageService
.getRestService()
.getBuilder(GetMonitoringFullPageData.class)
.withURIVariable(API.PARAM_PARENT_MODEL_ID, String.valueOf(examId));
this.guiUpdates = guiUpdates;
this.statusFilter = EnumSet.noneOf(ConnectionStatus.class);
loadStatusFilter();
}
public void start(final PageContext pageContext, final Composite anchor, final long pollInterval) {
try {
final UpdateErrorHandler updateErrorHandler =
new UpdateErrorHandler(this.pageService, pageContext);
this.pushContext = new ServerPushContext(
anchor,
Utils.truePredicate(),
updateErrorHandler);
this.serverPushService.runServerPush(
this.pushContext,
pollInterval,
context -> update());
} catch (final Exception e) {
log.error("Failed to start FullPageMonitoringUpdate: ", e);
}
}
@Override
public EnumSet<ConnectionStatus> getStatusFilter() {
return this.statusFilter;
}
@Override
public String getStatusFilterParam() {
return this.statusFilterParam;
}
@Override
public boolean statusFilterChanged() {
return this.statusFilterChanged;
}
@Override
public void resetStatusFilterChanged() {
this.statusFilterChanged = false;
}
@Override
public boolean isStatusHidden(final ConnectionStatus status) {
return this.statusFilter.contains(status);
}
@Override
public void hideStatus(final ConnectionStatus status) {
this.statusFilter.add(status);
saveStatusFilter();
}
@Override
public void showStatus(final ConnectionStatus status) {
this.statusFilter.remove(status);
saveStatusFilter();
}
@Override
public MonitoringFullPageData getMonitoringFullPageData() {
return this.monitoringFullPageData;
}
private void update() {
if (this.updateInProgress) {
return;
}
this.updateInProgress = true;
this.asyncRunner.runAsync(() -> {
try {
updateBusinessData();
} catch (final Exception e) {
log.error("Failed to update full page monitoring: ", e);
} finally {
this.updateInProgress = false;
}
});
if (this.monitoringFullPageData != null) {
callGUIUpdates();
}
}
private void updateBusinessData() {
this.monitoringFullPageData = this.restCallBuilder
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam)
.call()
.get(error -> {
recoverFromDisposedRestTemplate(error);
this.pushContext.reportError(error);
return this.monitoringFullPageData;
});
}
private void callGUIUpdates() {
this.guiUpdates.forEach(updater -> {
try {
updater.update(this);
} catch (final Exception e) {
log.error("Failed to update monitoring GUI element: ", e);
this.pushContext.reportError(e);
}
});
}
private void saveStatusFilter() {
try {
this.pageService
.getCurrentUser()
.putAttribute(
USER_SESSION_STATUS_FILTER_ATTRIBUTE,
StringUtils.join(this.statusFilter, 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;
}
}
private void loadStatusFilter() {
try {
final String attribute = this.pageService
.getCurrentUser()
.getAttribute(USER_SESSION_STATUS_FILTER_ATTRIBUTE);
this.statusFilter.clear();
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);
}
} catch (final Exception e) {
log.warn("Failed to load status filter to user session");
this.statusFilter.clear();
this.statusFilter.add(ConnectionStatus.DISABLED);
} finally {
this.statusFilterParam = StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR);
this.statusFilterChanged = true;
}
}
public void recoverFromDisposedRestTemplate(final Exception error) {
if (log.isDebugEnabled()) {
log.debug("Try to recover from disposed OAuth2 rest template...");
}
if (error instanceof DisposedOAuth2RestTemplateException) {
this.pageService.getRestService().injectCurrentRestTemplate(this.restCallBuilder);
}
}
}

View file

@ -0,0 +1,75 @@
/*
* 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.service.session;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.MonitoringFullPageData;
import ch.ethz.seb.sebserver.gbl.model.session.MonitoringSEBConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
public interface MonitoringStatus {
EnumSet<ConnectionStatus> getStatusFilter();
String getStatusFilterParam();
boolean statusFilterChanged();
void resetStatusFilterChanged();
boolean isStatusHidden(ConnectionStatus status);
void hideStatus(ConnectionStatus status);
void showStatus(ConnectionStatus status);
MonitoringFullPageData getMonitoringFullPageData();
default MonitoringSEBConnectionData getMonitoringSEBConnectionData() {
final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData();
if (monitoringFullPageData != null) {
return monitoringFullPageData.monitoringConnectionData;
} else {
return null;
}
}
default Collection<ClientConnectionData> getConnectionData() {
final MonitoringSEBConnectionData monitoringSEBConnectionData = getMonitoringSEBConnectionData();
if (monitoringSEBConnectionData != null) {
return monitoringSEBConnectionData.connections;
} else {
return Collections.emptyList();
}
}
default int getNumOfConnections(final ConnectionStatus status) {
final MonitoringSEBConnectionData monitoringSEBConnectionData = getMonitoringSEBConnectionData();
if (monitoringSEBConnectionData != null) {
return monitoringSEBConnectionData.getNumberOfConnection(status);
} else {
return 0;
}
}
default Collection<RemoteProctoringRoom> proctoringData() {
final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData();
if (monitoringFullPageData != null) {
return monitoringFullPageData.proctoringData;
} else {
return null;
}
}
}

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.gui.service.session.proctoring;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
@ -53,7 +54,6 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable;
@ -151,15 +151,23 @@ public class MonitoringProctoringService {
}
public void initCollectingRoomActions(
final ServerPushContext pushContext,
final PageContext pageContext,
final PageActionBuilder actionBuilder,
final ProctoringServiceSettings proctoringSettings,
final ProctoringGUIService proctoringGUIService) {
proctoringGUIService.clearCollectingRoomActionState();
final EntityKey entityKey = pageContext.getEntityKey();
final Collection<RemoteProctoringRoom> collectingRooms = this.pageService
.getRestService()
.getBuilder(GetCollectingRooms.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> log.error("Failed to get collecting room data:", error))
.getOr(Collections.emptyList());
updateCollectingRoomActions(
pushContext,
collectingRooms,
pageContext,
actionBuilder,
proctoringSettings,
@ -167,7 +175,7 @@ public class MonitoringProctoringService {
}
public void updateCollectingRoomActions(
final ServerPushContext pushContext,
final Collection<RemoteProctoringRoom> collectingRooms,
final PageContext pageContext,
final PageActionBuilder actionBuilder,
final ProctoringServiceSettings proctoringSettings,
@ -176,13 +184,7 @@ public class MonitoringProctoringService {
final EntityKey entityKey = pageContext.getEntityKey();
final I18nSupport i18nSupport = this.pageService.getI18nSupport();
this.pageService
.getRestService()
.getBuilder(GetCollectingRooms.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> pushContext.reportError(error))
.getOr(Collections.emptyList())
collectingRooms
.stream()
.forEach(room -> {
if (proctoringGUIService.collectingRoomActionActive(room.name)) {

View file

@ -19,6 +19,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
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.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.MonitoringSEBConnectionData;
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.ExamDAO;
@ -80,7 +81,15 @@ public interface ExamSessionService {
*
* @param examId The Exam identifier
* @return true if the given Exam has currently no active client connection, false otherwise. */
boolean hasActiveSEBClientConnections(final Long examId);
default boolean hasActiveSEBClientConnections(final Long examId) {
if (examId == null || !this.isExamRunning(examId)) {
return false;
}
return !this.getActiveConnectionTokens(examId)
.getOrThrow()
.isEmpty();
}
/** Checks if a specified Exam has at least a default SEB Exam configuration attached.
*
@ -144,12 +153,8 @@ public interface ExamSessionService {
* @return Result refer to the ClientConnectionData instance or to an error if happened */
Result<ClientConnectionData> getConnectionData(String connectionToken);
/** Get the collection of ClientConnectionData of all active SEB client connections
* of a running exam.
*
* active SEB client connections are connections that were initialized by a SEB client
* on the particular server instance. This may not be the all connections of an exam but
* a subset of them.
/** Get the collection of ClientConnectionData of all SEB client connections
* of a running exam that match the given filter criteria.
*
* @param examId The exam identifier
* @param filter a filter predicate to apply
@ -159,12 +164,23 @@ public interface ExamSessionService {
Long examId,
Predicate<ClientConnectionData> filter);
/** Get the MonitoringSEBConnectionsData containing a collection of ClientConnectionData of all
* SEB client connections matching the given filter criteria.
* And also containing a connection number per connection status mapping.
*
* @param examId The exam identifier
* @param filter a filter predicate to apply
* @return Result refer to MonitoringSEBConnectionsData of a running exam or to an error when happened */
Result<MonitoringSEBConnectionData> getMonitoringSEBConnectionsData(
final Long examId,
final Predicate<ClientConnectionData> filter);
/** Gets all connection tokens of active client connection that are related to a specified exam
* from persistence storage without caching involved.
*
* @param examId the exam identifier
* @return Result refer to the collection of connection tokens or to an error when happened. */
Result<Collection<String>> getActiveConnectionTokens(final Long examId);
Result<Collection<String>> getActiveConnectionTokens(Long examId);
/** Use this to check if the current cached running exam is up to date
* and if not to flush the cache.

View file

@ -285,14 +285,11 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
// check if the configuration is attached to a running exams with active client connections
final long activeConnections = involvedExams
.stream()
.flatMap(examId -> this.examSessionService
.getConnectionData(examId, ExamSessionService::isActiveConnection)
.getOrThrow()
.stream())
.filter(examId -> this.examSessionService.hasActiveSEBClientConnections(examId))
.count();
// if we have active SEB client connection on any running exam that
// is involved within the specified configuration change, the change is denied
// if we have active SEB client connection on one or more running exam that
// are involved within the specified configuration change, the change is denied
if (activeConnections > 0) {
return Result.ofError(new APIMessage.APIMessageException(
ErrorMessage.INTEGRITY_VALIDATION,

View file

@ -13,6 +13,7 @@ import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
@ -34,7 +35,9 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
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;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.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;
@ -163,17 +166,6 @@ public class ExamSessionServiceImpl implements ExamSessionService {
});
}
@Override
public boolean hasActiveSEBClientConnections(final Long examId) {
if (examId == null || !this.isExamRunning(examId)) {
return false;
}
return !this.getConnectionData(examId, ExamSessionService::isActiveConnection)
.getOrThrow()
.isEmpty();
}
@Override
public boolean hasDefaultConfigurationAttached(final Long examId) {
return !this.examConfigurationMapDAO
@ -360,6 +352,38 @@ public class ExamSessionServiceImpl implements ExamSessionService {
});
}
@Override
public Result<MonitoringSEBConnectionData> getMonitoringSEBConnectionsData(
final Long examId,
final Predicate<ClientConnectionData> filter) {
return Result.tryCatch(() -> {
// needed to store connection numbers per status
final int[] statusMapping = new int[ConnectionStatus.values().length];
for (int i = 0; i < statusMapping.length; i++) {
statusMapping[i] = 0;
}
updateClientConnections(examId);
final List<ClientConnectionData> filteredConnections = this.clientConnectionDAO
.getConnectionTokens(examId)
.getOrThrow()
.stream()
.map(token -> getConnectionData(token).getOr(null))
.filter(Objects::nonNull)
.map(c -> {
statusMapping[c.clientConnection.status.code]++;
return c;
})
.filter(filter)
.collect(Collectors.toList());
return new MonitoringSEBConnectionData(examId, filteredConnections, statusMapping);
});
}
@Override
public Result<Collection<String>> getActiveConnectionTokens(final Long examId) {
return this.clientConnectionDAO

View file

@ -49,7 +49,7 @@ public abstract class AbstractLogLevelCountIndicator extends AbstractLogIndicato
@Override
public final boolean hasIncident() {
return this.currentValue > this.incidentThreshold;
return this.currentValue >= this.incidentThreshold;
}
@Override

View file

@ -50,7 +50,7 @@ public class BatteryStatusIndicator extends AbstractLogNumberIndicator {
@Override
public final boolean hasIncident() {
return this.currentValue < this.incidentThreshold;
return this.currentValue <= this.incidentThreshold;
}
}

View file

@ -115,7 +115,7 @@ public final class PingIntervalClientIndicator extends AbstractPingIndicator {
@Override
public final boolean hasIncident() {
return getValue() > super.incidentThreshold;
return getValue() >= super.incidentThreshold;
}
private double lastCheckVal = 0;

View file

@ -50,7 +50,7 @@ public class WLANStatusIndicator extends AbstractLogNumberIndicator {
@Override
public final boolean hasIncident() {
return this.currentValue < this.incidentThreshold;
return this.currentValue <= this.incidentThreshold;
}
}

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Objects;
import java.util.concurrent.Executor;
@ -45,6 +46,9 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus
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.ClientNotification;
import ch.ethz.seb.sebserver.gbl.model.session.MonitoringSEBConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.MonitoringFullPageData;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -53,6 +57,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.Authorization
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
@ -71,6 +77,8 @@ public class ExamMonitoringController {
private final AuthorizationService authorization;
private final PaginationService paginationService;
private final SEBClientNotificationService sebClientNotificationService;
private final ExamProctoringRoomService examProcotringRoomService;
private final ExamAdminService examAdminService;
private final Executor executor;
public ExamMonitoringController(
@ -79,6 +87,8 @@ public class ExamMonitoringController {
final AuthorizationService authorization,
final PaginationService paginationService,
final SEBClientNotificationService sebClientNotificationService,
final ExamProctoringRoomService examProcotringRoomService,
final ExamAdminService examAdminService,
@Qualifier(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) final Executor executor) {
this.sebClientConnectionService = sebClientConnectionService;
@ -87,6 +97,8 @@ public class ExamMonitoringController {
this.authorization = authorization;
this.paginationService = paginationService;
this.sebClientNotificationService = sebClientNotificationService;
this.examProcotringRoomService = examProcotringRoomService;
this.examAdminService = examAdminService;
this.executor = executor;
}
@ -191,7 +203,47 @@ public class ExamMonitoringController {
}
return this.examSessionService
.getConnectionData(
.getMonitoringSEBConnectionsData(
examId,
filterStates.isEmpty()
? Objects::nonNull
: active
? withActiveFilter(filterStates)
: noneActiveFilter(filterStates))
.getOrThrow().connections;
}
@RequestMapping(
path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT +
API.EXAM_MONITORING_FULLPAGE,
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public MonitoringFullPageData getFullpageData(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
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) {
checkPrivileges(institutionId, examId);
final EnumSet<ConnectionStatus> 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
@ -199,28 +251,22 @@ public class ExamMonitoringController {
? withActiveFilter(filterStates)
: noneActiveFilter(filterStates))
.getOrThrow();
}
private Predicate<ClientConnectionData> noneActiveFilter(final EnumSet<ConnectionStatus> filterStates) {
return conn -> conn != null && !filterStates.contains(conn.clientConnection.status);
}
if (this.examAdminService.isProctoringEnabled(examId).getOr(false)) {
final Collection<RemoteProctoringRoom> proctoringData = this.examProcotringRoomService
.getProctoringCollectingRooms(examId)
.getOrThrow();
/** If we have a filter criteria for ACTIVE connection, we shall filter only the active connections
* that has no incident. */
private Predicate<ClientConnectionData> withActiveFilter(final EnumSet<ConnectionStatus> filterStates) {
return conn -> {
if (conn == null) {
return false;
} else if (conn.clientConnection.status == ConnectionStatus.ACTIVE) {
return conn.hasAnyIncident();
} else {
return !filterStates.contains(conn.clientConnection.status);
}
};
// return conn -> conn != null
// && ((conn.clientConnection.status == ConnectionStatus.ACTIVE && !conn.hasAnyIncident()) ||
// (conn.clientConnection.status != ConnectionStatus.ACTIVE
// && !filterStates.contains(conn.clientConnection.status)));
return new MonitoringFullPageData(
examId,
monitoringSEBConnectionData,
proctoringData);
} else {
return new MonitoringFullPageData(
examId,
monitoringSEBConnectionData,
Collections.emptyList());
}
}
@RequestMapping(
@ -380,4 +426,22 @@ public class ExamMonitoringController {
&& (exam.isOwner(userId) || userInfo.hasRole(UserRole.EXAM_ADMIN));
}
private Predicate<ClientConnectionData> noneActiveFilter(final EnumSet<ConnectionStatus> filterStates) {
return conn -> conn != null && !filterStates.contains(conn.clientConnection.status);
}
/** If we have a filter criteria for ACTIVE connection, we shall filter only the active connections
* that has no incident. */
private Predicate<ClientConnectionData> withActiveFilter(final EnumSet<ConnectionStatus> filterStates) {
return conn -> {
if (conn == null) {
return false;
} else if (conn.clientConnection.status == ConnectionStatus.ACTIVE) {
return conn.hasAnyIncident();
} else {
return !filterStates.contains(conn.clientConnection.status);
}
};
}
}

View file

@ -1797,16 +1797,16 @@ sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm=Are you
sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm=Are you sure to disable all selected SEB client connections?
sebserver.monitoring.exam.connection.action.instruction.disable.all.confirm=Are you sure to disable all active SEB client connections?
sebserver.monitoring.exam.connection.action.disable=Mark As Canceled
sebserver.monitoring.exam.connection.action.hide.requested=Hide Requested
sebserver.monitoring.exam.connection.action.show.requested=Show Requested
sebserver.monitoring.exam.connection.action.hide.active=Hide Active
sebserver.monitoring.exam.connection.action.show.active=Show Active
sebserver.monitoring.exam.connection.action.hide.closed=Hide Closed
sebserver.monitoring.exam.connection.action.show.closed=Show Closed
sebserver.monitoring.exam.connection.action.hide.disabled=Hide Canceled
sebserver.monitoring.exam.connection.action.show.disabled=Show Canceled
sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined
sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined
sebserver.monitoring.exam.connection.action.hide.requested=Hide Requested ({0})
sebserver.monitoring.exam.connection.action.show.requested=Show Requested ({0})
sebserver.monitoring.exam.connection.action.hide.active=Hide Active ({0})
sebserver.monitoring.exam.connection.action.show.active=Show Active ({0})
sebserver.monitoring.exam.connection.action.hide.closed=Hide Closed ({0})
sebserver.monitoring.exam.connection.action.show.closed=Show Closed ({0})
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.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.<br/>Are you sure to open the town-hall?

View file

@ -28,6 +28,7 @@ import org.apache.commons.codec.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.assertj.core.util.Arrays;
import org.joda.time.DateTimeZone;
import org.junit.After;
import org.junit.Before;
@ -90,6 +91,8 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue;
import ch.ethz.seb.sebserver.gbl.model.session.MonitoringFullPageData;
import ch.ethz.seb.sebserver.gbl.model.session.MonitoringSEBConnectionData;
import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
@ -188,6 +191,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.Sa
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.SaveExamConfigValue;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.DisableClientConnection;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetMonitoringFullPageData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetRunningExamPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.PropagateInstruction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.ActivateUserAccount;
@ -2043,6 +2047,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
"examSupport2",
new GetRunningExamPage(),
new GetClientConnectionDataList(),
new GetMonitoringFullPageData(),
new GetExtendedClientEventPage(),
new DisableClientConnection(),
new PropagateInstruction());
@ -2079,6 +2084,18 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
// no SEB connections available yet
assertTrue(connections.isEmpty());
// get MonitoringFullPageData
final Result<MonitoringFullPageData> fullPageData = restService.getBuilder(GetMonitoringFullPageData.class)
.withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId())
.call();
assertNotNull(fullPageData);
assertFalse(fullPageData.hasError());
final MonitoringSEBConnectionData monitoringConnectionData = fullPageData.get().monitoringConnectionData;
assertTrue(monitoringConnectionData.connections.isEmpty());
assertEquals(
"[0, 0, 0, 0, 0, 0]",
String.valueOf(Arrays.asList(monitoringConnectionData.connectionsPerStatus)));
// get active client config's credentials
final Result<Page<SEBClientConfig>> cconfigs = adminRestService.getBuilder(GetClientConfigPage.class)
.call();