SEBSERV-147 GUI implementation
This commit is contained in:
		
							parent
							
								
									8e10995f0d
								
							
						
					
					
						commit
						3052ec1f59
					
				
					 24 changed files with 329 additions and 69 deletions
				
			
		| 
						 | 
					@ -100,7 +100,6 @@ public final class Indicator implements Entity {
 | 
				
			||||||
    public final String defaultIcon;
 | 
					    public final String defaultIcon;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @JsonProperty(INDICATOR.ATTR_TAGS)
 | 
					    @JsonProperty(INDICATOR.ATTR_TAGS)
 | 
				
			||||||
    @Size(min = 3, max = 255, message = "indicator:tag:size:{min}:{max}:${validatedValue}")
 | 
					 | 
				
			||||||
    public final String tags;
 | 
					    public final String tags;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @JsonProperty(THRESHOLD.REFERENCE_NAME)
 | 
					    @JsonProperty(THRESHOLD.REFERENCE_NAME)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,14 +33,17 @@ public class ClientConnectionData {
 | 
				
			||||||
    public final List<? extends IndicatorValue> indicatorValues;
 | 
					    public final List<? extends IndicatorValue> indicatorValues;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public final Boolean missingPing;
 | 
					    public final Boolean missingPing;
 | 
				
			||||||
 | 
					    public final Boolean pendingNotification;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @JsonCreator
 | 
					    @JsonCreator
 | 
				
			||||||
    public ClientConnectionData(
 | 
					    public ClientConnectionData(
 | 
				
			||||||
            @JsonProperty(ATTR_MISSING_PING) final Boolean missingPing,
 | 
					            @JsonProperty(ATTR_MISSING_PING) final Boolean missingPing,
 | 
				
			||||||
 | 
					            @JsonProperty(ATTR_PENDING_NOTIFICATION) final Boolean pendingNotification,
 | 
				
			||||||
            @JsonProperty(ATTR_CLIENT_CONNECTION) final ClientConnection clientConnection,
 | 
					            @JsonProperty(ATTR_CLIENT_CONNECTION) final ClientConnection clientConnection,
 | 
				
			||||||
            @JsonProperty(ATTR_INDICATOR_VALUE) final Collection<? extends SimpleIndicatorValue> indicatorValues) {
 | 
					            @JsonProperty(ATTR_INDICATOR_VALUE) final Collection<? extends SimpleIndicatorValue> indicatorValues) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.missingPing = missingPing;
 | 
					        this.missingPing = missingPing;
 | 
				
			||||||
 | 
					        this.pendingNotification = pendingNotification;
 | 
				
			||||||
        this.clientConnection = clientConnection;
 | 
					        this.clientConnection = clientConnection;
 | 
				
			||||||
        this.indicatorValues = Utils.immutableListOf(indicatorValues);
 | 
					        this.indicatorValues = Utils.immutableListOf(indicatorValues);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -50,6 +53,7 @@ public class ClientConnectionData {
 | 
				
			||||||
            final List<? extends IndicatorValue> indicatorValues) {
 | 
					            final List<? extends IndicatorValue> indicatorValues) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.missingPing = null;
 | 
					        this.missingPing = null;
 | 
				
			||||||
 | 
					        this.pendingNotification = Boolean.FALSE;
 | 
				
			||||||
        this.clientConnection = clientConnection;
 | 
					        this.clientConnection = clientConnection;
 | 
				
			||||||
        this.indicatorValues = Utils.immutableListOf(indicatorValues);
 | 
					        this.indicatorValues = Utils.immutableListOf(indicatorValues);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -61,7 +65,7 @@ public class ClientConnectionData {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @JsonProperty(ATTR_PENDING_NOTIFICATION)
 | 
					    @JsonProperty(ATTR_PENDING_NOTIFICATION)
 | 
				
			||||||
    public Boolean pendingNotification() {
 | 
					    public Boolean pendingNotification() {
 | 
				
			||||||
        return false;
 | 
					        return this.pendingNotification;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @JsonIgnore
 | 
					    @JsonIgnore
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,8 @@ public final class ClientInstruction {
 | 
				
			||||||
    public enum InstructionType {
 | 
					    public enum InstructionType {
 | 
				
			||||||
        SEB_QUIT,
 | 
					        SEB_QUIT,
 | 
				
			||||||
        SEB_PROCTORING,
 | 
					        SEB_PROCTORING,
 | 
				
			||||||
        SEB_RECONFIGURE_SETTINGS
 | 
					        SEB_RECONFIGURE_SETTINGS,
 | 
				
			||||||
 | 
					        NOTIFICATION_CONFIRM
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public enum ProctoringInstructionMethod {
 | 
					    public enum ProctoringInstructionMethod {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,109 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2020 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 org.apache.commons.lang3.StringUtils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonCreator;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonProperty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.Constants;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.Domain;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ClientNotification extends ClientEvent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static enum NotificationType {
 | 
				
			||||||
 | 
					        UNKNOWN(null),
 | 
				
			||||||
 | 
					        LOCK_SCREEN("lockscreen"),
 | 
				
			||||||
 | 
					        RAISE_HAND("raisehand");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public final String typeName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private NotificationType(final String typeName) {
 | 
				
			||||||
 | 
					            this.typeName = typeName;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public static NotificationType getNotificationType(final String text) {
 | 
				
			||||||
 | 
					            if (StringUtils.isBlank(text)) {
 | 
				
			||||||
 | 
					                return NotificationType.UNKNOWN;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return Arrays.asList(NotificationType.values())
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    .filter(type -> type.typeName != null &&
 | 
				
			||||||
 | 
					                            text.startsWith(Constants.ANGLE_BRACE_OPEN + type.typeName + Constants.ANGLE_BRACE_CLOSE))
 | 
				
			||||||
 | 
					                    .findFirst()
 | 
				
			||||||
 | 
					                    .orElse(NotificationType.UNKNOWN);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static final String ATTR_NOTIFICATION_TYPE = "notificationType";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @JsonProperty(ATTR_NOTIFICATION_TYPE)
 | 
				
			||||||
 | 
					    public final NotificationType notificationType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ClientNotification(
 | 
				
			||||||
 | 
					            final Long id,
 | 
				
			||||||
 | 
					            final Long connectionId,
 | 
				
			||||||
 | 
					            final EventType eventType,
 | 
				
			||||||
 | 
					            final Long clientTime,
 | 
				
			||||||
 | 
					            final Long serverTime,
 | 
				
			||||||
 | 
					            final Double numValue,
 | 
				
			||||||
 | 
					            final String text) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super(id, connectionId, eventType, clientTime, serverTime, numValue, text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.notificationType = NotificationType.getNotificationType(text);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @JsonCreator
 | 
				
			||||||
 | 
					    public ClientNotification(
 | 
				
			||||||
 | 
					            @JsonProperty(Domain.CLIENT_EVENT.ATTR_ID) final Long id,
 | 
				
			||||||
 | 
					            @JsonProperty(Domain.CLIENT_EVENT.ATTR_CLIENT_CONNECTION_ID) final Long connectionId,
 | 
				
			||||||
 | 
					            @JsonProperty(Domain.CLIENT_EVENT.ATTR_TYPE) final EventType eventType,
 | 
				
			||||||
 | 
					            @JsonProperty(ATTR_TIMESTAMP) final Long clientTime,
 | 
				
			||||||
 | 
					            @JsonProperty(Domain.CLIENT_EVENT.ATTR_SERVER_TIME) final Long serverTime,
 | 
				
			||||||
 | 
					            @JsonProperty(Domain.CLIENT_EVENT.ATTR_NUMERIC_VALUE) final Double numValue,
 | 
				
			||||||
 | 
					            @JsonProperty(Domain.CLIENT_EVENT.ATTR_TEXT) final String text,
 | 
				
			||||||
 | 
					            @JsonProperty(ATTR_NOTIFICATION_TYPE) final NotificationType notificationType) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super(id, connectionId, eventType, clientTime, serverTime, numValue, text);
 | 
				
			||||||
 | 
					        this.notificationType = notificationType;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public NotificationType getNotificationType() {
 | 
				
			||||||
 | 
					        return this.notificationType;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public String toString() {
 | 
				
			||||||
 | 
					        final StringBuilder builder = new StringBuilder();
 | 
				
			||||||
 | 
					        builder.append("ClientNotification [notificationType=");
 | 
				
			||||||
 | 
					        builder.append(this.notificationType);
 | 
				
			||||||
 | 
					        builder.append(", id=");
 | 
				
			||||||
 | 
					        builder.append(this.id);
 | 
				
			||||||
 | 
					        builder.append(", connectionId=");
 | 
				
			||||||
 | 
					        builder.append(this.connectionId);
 | 
				
			||||||
 | 
					        builder.append(", eventType=");
 | 
				
			||||||
 | 
					        builder.append(this.eventType);
 | 
				
			||||||
 | 
					        builder.append(", clientTime=");
 | 
				
			||||||
 | 
					        builder.append(this.clientTime);
 | 
				
			||||||
 | 
					        builder.append(", serverTime=");
 | 
				
			||||||
 | 
					        builder.append(this.serverTime);
 | 
				
			||||||
 | 
					        builder.append(", numValue=");
 | 
				
			||||||
 | 
					        builder.append(this.numValue);
 | 
				
			||||||
 | 
					        builder.append(", text=");
 | 
				
			||||||
 | 
					        builder.append(this.text);
 | 
				
			||||||
 | 
					        builder.append("]");
 | 
				
			||||||
 | 
					        return builder.toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -35,6 +35,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
 | 
				
			||||||
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.ClientConnectionData;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
 | 
				
			||||||
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.user.UserRole;
 | 
					import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
 | 
				
			||||||
| 
						 | 
					@ -88,7 +89,11 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
    private static final LocTextKey NOTIFICATION_LIST_TITLE_TOOLTIP_KEY =
 | 
					    private static final LocTextKey NOTIFICATION_LIST_TITLE_TOOLTIP_KEY =
 | 
				
			||||||
            new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.title.tooltip");
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.title.tooltip");
 | 
				
			||||||
    private static final LocTextKey NOTIFICATION_LIST_CONFIRM_TEXT_KEY =
 | 
					    private static final LocTextKey NOTIFICATION_LIST_CONFIRM_TEXT_KEY =
 | 
				
			||||||
            new LocTextKey("monitoring.exam.connection.action.confirm.notification.text");
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.action.confirm.notification.text");
 | 
				
			||||||
 | 
					    private static final LocTextKey NOTIFICATION_LIST_NO_SELECTION_KEY =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.pleaseSelect");
 | 
				
			||||||
 | 
					    private static final LocTextKey NOTIFICATION_LIST_COLUMN_TYPE_KEY =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.type");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final LocTextKey EVENT_LIST_TITLE_KEY =
 | 
					    private static final LocTextKey EVENT_LIST_TITLE_KEY =
 | 
				
			||||||
            new LocTextKey("sebserver.monitoring.exam.connection.eventlist.title");
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.eventlist.title");
 | 
				
			||||||
| 
						 | 
					@ -98,6 +103,7 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
            new LocTextKey("sebserver.monitoring.exam.connection.eventlist.empty");
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.eventlist.empty");
 | 
				
			||||||
    private static final LocTextKey LIST_COLUMN_TYPE_KEY =
 | 
					    private static final LocTextKey LIST_COLUMN_TYPE_KEY =
 | 
				
			||||||
            new LocTextKey("sebserver.monitoring.exam.connection.eventlist.type");
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.eventlist.type");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final LocTextKey LIST_COLUMN_CLIENT_TIME_KEY =
 | 
					    private static final LocTextKey LIST_COLUMN_CLIENT_TIME_KEY =
 | 
				
			||||||
            new LocTextKey("sebserver.monitoring.exam.connection.eventlist.clienttime");
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.eventlist.clienttime");
 | 
				
			||||||
    private static final LocTextKey LIST_COLUMN_SERVER_TIME_KEY =
 | 
					    private static final LocTextKey LIST_COLUMN_SERVER_TIME_KEY =
 | 
				
			||||||
| 
						 | 
					@ -212,6 +218,7 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
                                .clearAttributes()
 | 
					                                .clearAttributes()
 | 
				
			||||||
                                .clearEntityKeys());
 | 
					                                .clearEntityKeys());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // NOTIFICATIONS
 | 
				
			||||||
        final boolean hasNotification = BooleanUtils.isTrue(connectionData.pendingNotification());
 | 
					        final boolean hasNotification = BooleanUtils.isTrue(connectionData.pendingNotification());
 | 
				
			||||||
        if (hasNotification) {
 | 
					        if (hasNotification) {
 | 
				
			||||||
            // add notification table
 | 
					            // add notification table
 | 
				
			||||||
| 
						 | 
					@ -221,7 +228,7 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
                    NOTIFICATION_LIST_TITLE_KEY,
 | 
					                    NOTIFICATION_LIST_TITLE_KEY,
 | 
				
			||||||
                    NOTIFICATION_LIST_TITLE_TOOLTIP_KEY);
 | 
					                    NOTIFICATION_LIST_TITLE_TOOLTIP_KEY);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            final EntityTable<ClientEvent> notificationTable = this.pageService.remoteListTableBuilder(
 | 
					            final EntityTable<ClientNotification> notificationTable = this.pageService.remoteListTableBuilder(
 | 
				
			||||||
                    restService.getRestCall(GetPendingClientNotifications.class),
 | 
					                    restService.getRestCall(GetPendingClientNotifications.class),
 | 
				
			||||||
                    EntityType.CLIENT_EVENT)
 | 
					                    EntityType.CLIENT_EVENT)
 | 
				
			||||||
                    .withRestCallAdapter(builder -> builder.withURIVariable(
 | 
					                    .withRestCallAdapter(builder -> builder.withURIVariable(
 | 
				
			||||||
| 
						 | 
					@ -230,22 +237,22 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
                            .withURIVariable(
 | 
					                            .withURIVariable(
 | 
				
			||||||
                                    API.EXAM_API_SEB_CONNECTION_TOKEN,
 | 
					                                    API.EXAM_API_SEB_CONNECTION_TOKEN,
 | 
				
			||||||
                                    connectionToken))
 | 
					                                    connectionToken))
 | 
				
			||||||
                    .withPaging(5)
 | 
					                    .withPaging(-1)
 | 
				
			||||||
                    .withColumn(new ColumnDefinition<ClientEvent>(
 | 
					                    .withColumn(new ColumnDefinition<ClientNotification>(
 | 
				
			||||||
                            Domain.CLIENT_EVENT.ATTR_TYPE,
 | 
					                            ClientNotification.ATTR_NOTIFICATION_TYPE,
 | 
				
			||||||
                            LIST_COLUMN_TYPE_KEY,
 | 
					                            NOTIFICATION_LIST_COLUMN_TYPE_KEY,
 | 
				
			||||||
                            this.resourceService::getEventTypeName)
 | 
					                            this.resourceService::getNotificationTypeName)
 | 
				
			||||||
                                    .sortable()
 | 
					                                    .sortable()
 | 
				
			||||||
                                    .widthProportion(2))
 | 
					                                    .widthProportion(2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    .withColumn(new ColumnDefinition<>(
 | 
					                    .withColumn(new ColumnDefinition<ClientNotification>(
 | 
				
			||||||
                            Domain.CLIENT_EVENT.ATTR_TEXT,
 | 
					                            Domain.CLIENT_EVENT.ATTR_TEXT,
 | 
				
			||||||
                            LIST_COLUMN_TEXT_KEY,
 | 
					                            LIST_COLUMN_TEXT_KEY,
 | 
				
			||||||
                            ClientEvent::getText)
 | 
					                            ClientEvent::getText)
 | 
				
			||||||
                                    .sortable()
 | 
					                                    .sortable()
 | 
				
			||||||
                                    .withCellTooltip()
 | 
					                                    .withCellTooltip()
 | 
				
			||||||
                                    .widthProportion(4))
 | 
					                                    .widthProportion(4))
 | 
				
			||||||
                    .withColumn(new ColumnDefinition<>(
 | 
					                    .withColumn(new ColumnDefinition<ClientNotification>(
 | 
				
			||||||
                            Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
 | 
					                            Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
 | 
				
			||||||
                            new LocTextKey(LIST_COLUMN_SERVER_TIME_KEY.name,
 | 
					                            new LocTextKey(LIST_COLUMN_SERVER_TIME_KEY.name,
 | 
				
			||||||
                                    this.i18nSupport.getUsersTimeZoneTitleSuffix()),
 | 
					                                    this.i18nSupport.getUsersTimeZoneTitleSuffix()),
 | 
				
			||||||
| 
						 | 
					@ -268,10 +275,15 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
                    .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION)
 | 
					                    .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION)
 | 
				
			||||||
                    .withParentEntityKey(parentEntityKey)
 | 
					                    .withParentEntityKey(parentEntityKey)
 | 
				
			||||||
                    .withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY)
 | 
					                    .withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY)
 | 
				
			||||||
                    .withExec(action -> this.confirmNotification(action, connectionData))
 | 
					                    .withSelect(
 | 
				
			||||||
                    .publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER));
 | 
					                            () -> notificationTable.getSelection(),
 | 
				
			||||||
 | 
					                            action -> this.confirmNotification(action, connectionData),
 | 
				
			||||||
 | 
					                            NOTIFICATION_LIST_NO_SELECTION_KEY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    .publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // CLIENT EVENTS
 | 
				
			||||||
        widgetFactory.addFormSubContextHeader(
 | 
					        widgetFactory.addFormSubContextHeader(
 | 
				
			||||||
                content,
 | 
					                content,
 | 
				
			||||||
                EVENT_LIST_TITLE_KEY,
 | 
					                EVENT_LIST_TITLE_KEY,
 | 
				
			||||||
| 
						 | 
					@ -401,7 +413,13 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
                .call()
 | 
					                .call()
 | 
				
			||||||
                .getOrThrow();
 | 
					                .getOrThrow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return pageAction;
 | 
					        return pageAction
 | 
				
			||||||
 | 
					                .withEntityKey(
 | 
				
			||||||
 | 
					                        new EntityKey(connectionData.getConnectionId(),
 | 
				
			||||||
 | 
					                                EntityType.CLIENT_CONNECTION))
 | 
				
			||||||
 | 
					                .withAttribute(
 | 
				
			||||||
 | 
					                        Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
 | 
				
			||||||
 | 
					                        connectionData.clientConnection.connectionToken);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private PageAction openExamCollectionProctorScreen(
 | 
					    private PageAction openExamCollectionProctorScreen(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,8 @@ public enum ActionCategory {
 | 
				
			||||||
    SEB_CONFIG_TEMPLATE_LIST(new LocTextKey("sebserver.configtemplate.list.actions"), 1),
 | 
					    SEB_CONFIG_TEMPLATE_LIST(new LocTextKey("sebserver.configtemplate.list.actions"), 1),
 | 
				
			||||||
    SEB_CONFIG_TEMPLATE_ATTRIBUTE_LIST(new LocTextKey("sebserver.configtemplate.attr.list.actions"), 1),
 | 
					    SEB_CONFIG_TEMPLATE_ATTRIBUTE_LIST(new LocTextKey("sebserver.configtemplate.attr.list.actions"), 1),
 | 
				
			||||||
    RUNNING_EXAM_LIST(new LocTextKey("sebserver.monitoring.exam.list.actions"), 1),
 | 
					    RUNNING_EXAM_LIST(new LocTextKey("sebserver.monitoring.exam.list.actions"), 1),
 | 
				
			||||||
 | 
					    EXAM_MONITORING_NOTIFICATION_LIST(new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.actions"),
 | 
				
			||||||
 | 
					            1),
 | 
				
			||||||
    CLIENT_EVENT_LIST(new LocTextKey("sebserver.monitoring.exam.connection.list.actions"), 1),
 | 
					    CLIENT_EVENT_LIST(new LocTextKey("sebserver.monitoring.exam.connection.list.actions"), 1),
 | 
				
			||||||
    LOGS_USER_ACTIVITY_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),
 | 
					    LOGS_USER_ACTIVITY_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),
 | 
				
			||||||
    LOGS_SEB_CLIENT_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),
 | 
					    LOGS_SEB_CLIENT_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -644,7 +644,7 @@ public enum ActionDefinition {
 | 
				
			||||||
            new LocTextKey("sebserver.monitoring.exam.connection.action.confirm.notification"),
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.action.confirm.notification"),
 | 
				
			||||||
            ImageIcon.YES,
 | 
					            ImageIcon.YES,
 | 
				
			||||||
            PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
 | 
					            PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
 | 
				
			||||||
            ActionCategory.FORM),
 | 
					            ActionCategory.EXAM_MONITORING_NOTIFICATION_LIST),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    MONITOR_EXAM_QUIT_SELECTED(
 | 
					    MONITOR_EXAM_QUIT_SELECTED(
 | 
				
			||||||
            new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"),
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,6 +52,8 @@ 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.ClientConnectionData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification.NotificationType;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog;
 | 
					import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
 | 
					import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType;
 | 
					import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType;
 | 
				
			||||||
| 
						 | 
					@ -118,6 +120,8 @@ public class ResourceService {
 | 
				
			||||||
            AttributeType.INLINE_TABLE);
 | 
					            AttributeType.INLINE_TABLE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static final String CLIENT_EVENT_TYPE_PREFIX = "sebserver.monitoring.exam.connection.event.type.";
 | 
					    public static final String CLIENT_EVENT_TYPE_PREFIX = "sebserver.monitoring.exam.connection.event.type.";
 | 
				
			||||||
 | 
					    public static final String CLIENT_NOTIFICATION_TYPE_PREFIX =
 | 
				
			||||||
 | 
					            "sebserver.monitoring.exam.connection.notification.type.";
 | 
				
			||||||
    public static final String USER_ACTIVITY_TYPE_PREFIX = "sebserver.overall.types.activityType.";
 | 
					    public static final String USER_ACTIVITY_TYPE_PREFIX = "sebserver.overall.types.activityType.";
 | 
				
			||||||
    public static final String ENTITY_TYPE_PREFIX = "sebserver.overall.types.entityType.";
 | 
					    public static final String ENTITY_TYPE_PREFIX = "sebserver.overall.types.entityType.";
 | 
				
			||||||
    public static final String SEB_CONNECTION_STATUS_KEY_PREFIX = "sebserver.monitoring.exam.connection.status.";
 | 
					    public static final String SEB_CONNECTION_STATUS_KEY_PREFIX = "sebserver.monitoring.exam.connection.status.";
 | 
				
			||||||
| 
						 | 
					@ -200,6 +204,22 @@ public class ResourceService {
 | 
				
			||||||
        return this.i18nSupport.getText(CLIENT_EVENT_TYPE_PREFIX + eventType.name(), eventType.name());
 | 
					        return this.i18nSupport.getText(CLIENT_EVENT_TYPE_PREFIX + eventType.name(), eventType.name());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public String getNotificationTypeName(final ClientNotification notification) {
 | 
				
			||||||
 | 
					        if (notification == null) {
 | 
				
			||||||
 | 
					            return getEventTypeName(EventType.UNKNOWN);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return getNotificationTypeName(notification.getNotificationType());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public String getNotificationTypeName(final NotificationType notificationType) {
 | 
				
			||||||
 | 
					        if (notificationType == null) {
 | 
				
			||||||
 | 
					            return Constants.EMPTY_NOTE;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this.i18nSupport.getText(
 | 
				
			||||||
 | 
					                CLIENT_NOTIFICATION_TYPE_PREFIX + notificationType.name(),
 | 
				
			||||||
 | 
					                notificationType.name());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public List<Tuple<String>> indicatorTypeResources() {
 | 
					    public List<Tuple<String>> indicatorTypeResources() {
 | 
				
			||||||
        return Arrays.stream(IndicatorType.values())
 | 
					        return Arrays.stream(IndicatorType.values())
 | 
				
			||||||
                .map(type -> new Tuple3<>(
 | 
					                .map(type -> new Tuple3<>(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -339,6 +339,7 @@ public class PageContextImpl implements PageContext {
 | 
				
			||||||
                this.i18nSupport);
 | 
					                this.i18nSupport);
 | 
				
			||||||
        messageBox.setMarkupEnabled(true);
 | 
					        messageBox.setMarkupEnabled(true);
 | 
				
			||||||
        messageBox.open(null);
 | 
					        messageBox.open(null);
 | 
				
			||||||
 | 
					        log.error("Unexpected error on GUI: ", error);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,7 @@ public class ConfirmPendingClientNotification extends RestCall<Void> {
 | 
				
			||||||
                new TypeReference<Void>() {
 | 
					                new TypeReference<Void>() {
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
                HttpMethod.POST,
 | 
					                HttpMethod.POST,
 | 
				
			||||||
                MediaType.APPLICATION_JSON_UTF8,
 | 
					                MediaType.APPLICATION_FORM_URLENCODED,
 | 
				
			||||||
                API.EXAM_MONITORING_ENDPOINT
 | 
					                API.EXAM_MONITORING_ENDPOINT
 | 
				
			||||||
                        + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
 | 
					                        + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
 | 
				
			||||||
                        + API.EXAM_MONITORING_NOTIFICATION_ENDPOINT
 | 
					                        + API.EXAM_MONITORING_NOTIFICATION_ENDPOINT
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,20 +19,20 @@ import com.fasterxml.jackson.core.type.TypeReference;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.api.API;
 | 
					import ch.ethz.seb.sebserver.gbl.api.API;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
 | 
					import ch.ethz.seb.sebserver.gbl.api.EntityType;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 | 
					import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Lazy
 | 
					@Lazy
 | 
				
			||||||
@Component
 | 
					@Component
 | 
				
			||||||
@GuiProfile
 | 
					@GuiProfile
 | 
				
			||||||
public class GetPendingClientNotifications extends RestCall<Collection<ClientEvent>> {
 | 
					public class GetPendingClientNotifications extends RestCall<Collection<ClientNotification>> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public GetPendingClientNotifications() {
 | 
					    public GetPendingClientNotifications() {
 | 
				
			||||||
        super(new TypeKey<>(
 | 
					        super(new TypeKey<>(
 | 
				
			||||||
                CallType.GET_LIST,
 | 
					                CallType.GET_LIST,
 | 
				
			||||||
                EntityType.CLIENT_EVENT,
 | 
					                EntityType.CLIENT_EVENT,
 | 
				
			||||||
                new TypeReference<Collection<ClientEvent>>() {
 | 
					                new TypeReference<Collection<ClientNotification>>() {
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
                HttpMethod.GET,
 | 
					                HttpMethod.GET,
 | 
				
			||||||
                MediaType.APPLICATION_FORM_URLENCODED,
 | 
					                MediaType.APPLICATION_FORM_URLENCODED,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -513,13 +513,9 @@ public final class ClientConnectionTable {
 | 
				
			||||||
                if (list != null && list.size() > 1) {
 | 
					                if (list != null && list.size() > 1) {
 | 
				
			||||||
                    tableItem.setBackground(0, ClientConnectionTable.this.colorData.color3);
 | 
					                    tableItem.setBackground(0, ClientConnectionTable.this.colorData.color3);
 | 
				
			||||||
                    tableItem.setForeground(0, ClientConnectionTable.this.lightFontColor);
 | 
					                    tableItem.setForeground(0, ClientConnectionTable.this.lightFontColor);
 | 
				
			||||||
                    tableItem.setImage(1,
 | 
					 | 
				
			||||||
                            WidgetFactory.ImageIcon.ADD.getImage(ClientConnectionTable.this.table.getDisplay()));
 | 
					 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    tableItem.setBackground(0, null);
 | 
					                    tableItem.setBackground(0, null);
 | 
				
			||||||
                    tableItem.setForeground(0, ClientConnectionTable.this.darkFontColor);
 | 
					                    tableItem.setForeground(0, ClientConnectionTable.this.darkFontColor);
 | 
				
			||||||
                    tableItem.setImage(0, null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -218,7 +218,7 @@ public class EntityTable<ROW> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.table.addListener(SWT.Selection, event -> this.notifySelectionChange());
 | 
					        this.table.addListener(SWT.Selection, event -> this.notifySelectionChange());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.navigator = new TableNavigator(this);
 | 
					        this.navigator = (pageSize > 0) ? new TableNavigator(this) : new TableNavigator();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        createTableColumns();
 | 
					        createTableColumns();
 | 
				
			||||||
        this.pageNumber = initCurrentPageFromUserAttr();
 | 
					        this.pageNumber = initCurrentPageFromUserAttr();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -108,6 +108,10 @@ public class RemoteListPageSupplier<T> implements PageSupplier<T> {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                final int numOfPages = list.size() / this.pageSize;
 | 
					                final int numOfPages = list.size() / this.pageSize;
 | 
				
			||||||
 | 
					                if (numOfPages <= 0) {
 | 
				
			||||||
 | 
					                    return new Page<>(1, 1, this.column, list);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                final List<T> subList = list.subList(this.pageNumber * this.pageSize,
 | 
					                final List<T> subList = list.subList(this.pageNumber * this.pageSize,
 | 
				
			||||||
                        this.pageNumber * this.pageSize + this.pageSize);
 | 
					                        this.pageNumber * this.pageSize + this.pageSize);
 | 
				
			||||||
                return new Page<>(numOfPages, this.pageNumber, this.column, subList);
 | 
					                return new Page<>(numOfPages, this.pageNumber, this.column, subList);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -102,6 +102,10 @@ public class StaticListPageSupplier<T> implements PageSupplier<T> {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                final int numOfPages = this.list.size() / this.pageSize;
 | 
					                final int numOfPages = this.list.size() / this.pageSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (numOfPages <= 0) {
 | 
				
			||||||
 | 
					                    return new Page<>(1, 1, this.column, this.list);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                final List<T> subList = this.list.subList(this.pageNumber * this.pageSize,
 | 
					                final List<T> subList = this.list.subList(this.pageNumber * this.pageSize,
 | 
				
			||||||
                        this.pageNumber * this.pageSize + this.pageSize);
 | 
					                        this.pageNumber * this.pageSize + this.pageSize);
 | 
				
			||||||
                return new Page<>(numOfPages, this.pageNumber, this.column, subList);
 | 
					                return new Page<>(numOfPages, this.pageNumber, this.column, subList);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,11 @@ public class TableNavigator {
 | 
				
			||||||
    private final Composite composite;
 | 
					    private final Composite composite;
 | 
				
			||||||
    private final EntityTable<?> entityTable;
 | 
					    private final EntityTable<?> entityTable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    TableNavigator() {
 | 
				
			||||||
 | 
					        this.composite = null;
 | 
				
			||||||
 | 
					        this.entityTable = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    TableNavigator(final EntityTable<?> entityTable) {
 | 
					    TableNavigator(final EntityTable<?> entityTable) {
 | 
				
			||||||
        this.composite = new Composite(entityTable.composite, SWT.NONE);
 | 
					        this.composite = new Composite(entityTable.composite, SWT.NONE);
 | 
				
			||||||
        final GridData gridData = new GridData(SWT.LEFT, SWT.CENTER, true, true);
 | 
					        final GridData gridData = new GridData(SWT.LEFT, SWT.CENTER, true, true);
 | 
				
			||||||
| 
						 | 
					@ -37,6 +42,10 @@ public class TableNavigator {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Page<?> update(final Page<?> pageData) {
 | 
					    public Page<?> update(final Page<?> pageData) {
 | 
				
			||||||
 | 
					        if (this.composite == null) {
 | 
				
			||||||
 | 
					            return pageData;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // clear all
 | 
					        // clear all
 | 
				
			||||||
        PageService.clearComposite(this.composite);
 | 
					        PageService.clearComposite(this.composite);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ import java.util.List;
 | 
				
			||||||
import java.util.function.Predicate;
 | 
					import java.util.function.Predicate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,8 +28,10 @@ public interface ClientEventDAO extends EntityDAO<ClientEvent, ClientEvent> {
 | 
				
			||||||
            FilterMap filterMap,
 | 
					            FilterMap filterMap,
 | 
				
			||||||
            Predicate<ExtendedClientEvent> predicate);
 | 
					            Predicate<ExtendedClientEvent> predicate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Result<List<ClientEvent>> getPendingNotifications(Long clientConnectionId);
 | 
					    Result<ClientNotification> getPendingNotification(Long notificationId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Result<ClientEvent> confirmPendingNotification(Long notificationId, Long clientConnectionId);
 | 
					    Result<List<ClientNotification>> getPendingNotifications(Long clientConnectionId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Result<ClientNotification> confirmPendingNotification(Long notificationId, Long clientConnectionId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey;
 | 
				
			||||||
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.ClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 | 
					import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
| 
						 | 
					@ -187,21 +188,31 @@ public class ClientEventDAOImpl implements ClientEventDAO {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    @Transactional(readOnly = true)
 | 
					    @Transactional(readOnly = true)
 | 
				
			||||||
    public Result<List<ClientEvent>> getPendingNotifications(final Long clientConnectionId) {
 | 
					    public Result<ClientNotification> getPendingNotification(final Long notificationId) {
 | 
				
			||||||
        return Result.tryCatch(() -> this.clientEventRecordMapper.selectByExample()
 | 
					        return Result.tryCatch(() -> this.clientEventRecordMapper
 | 
				
			||||||
 | 
					                .selectByPrimaryKey(notificationId))
 | 
				
			||||||
 | 
					                .flatMap(ClientEventDAOImpl::toClientNotificationModel);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    @Transactional(readOnly = true)
 | 
				
			||||||
 | 
					    public Result<List<ClientNotification>> getPendingNotifications(final Long clientConnectionId) {
 | 
				
			||||||
 | 
					        return Result.tryCatch(() -> this.clientEventRecordMapper
 | 
				
			||||||
 | 
					                .selectByExample()
 | 
				
			||||||
                .where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(clientConnectionId))
 | 
					                .where(ClientEventRecordDynamicSqlSupport.clientConnectionId, isEqualTo(clientConnectionId))
 | 
				
			||||||
                .and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.NOTIFICATION.id))
 | 
					                .and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.NOTIFICATION.id))
 | 
				
			||||||
                .build()
 | 
					                .build()
 | 
				
			||||||
                .execute()
 | 
					                .execute()
 | 
				
			||||||
                .stream()
 | 
					                .stream()
 | 
				
			||||||
                .map(ClientEventDAOImpl::toDomainModel)
 | 
					                .map(ClientEventDAOImpl::toClientNotificationModel)
 | 
				
			||||||
                .flatMap(DAOLoggingSupport::logAndSkipOnError)
 | 
					                .flatMap(DAOLoggingSupport::logAndSkipOnError)
 | 
				
			||||||
                .collect(Collectors.toList()));
 | 
					                .collect(Collectors.toList()));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    @Transactional
 | 
					    @Transactional
 | 
				
			||||||
    public Result<ClientEvent> confirmPendingNotification(final Long notificationId, final Long clientConnectionId) {
 | 
					    public Result<ClientNotification> confirmPendingNotification(final Long notificationId,
 | 
				
			||||||
 | 
					            final Long clientConnectionId) {
 | 
				
			||||||
        return Result.tryCatch(() -> {
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
            final Long pk = this.clientEventRecordMapper.selectIdsByExample()
 | 
					            final Long pk = this.clientEventRecordMapper.selectIdsByExample()
 | 
				
			||||||
                    .where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(notificationId))
 | 
					                    .where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(notificationId))
 | 
				
			||||||
| 
						 | 
					@ -218,7 +229,7 @@ public class ClientEventDAOImpl implements ClientEventDAO {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return this.clientEventRecordMapper.selectByPrimaryKey(pk);
 | 
					            return this.clientEventRecordMapper.selectByPrimaryKey(pk);
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
                .flatMap(ClientEventDAOImpl::toDomainModel)
 | 
					                .flatMap(ClientEventDAOImpl::toClientNotificationModel)
 | 
				
			||||||
                .onError(TransactionHandler::rollback);
 | 
					                .onError(TransactionHandler::rollback);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -286,6 +297,22 @@ public class ClientEventDAOImpl implements ClientEventDAO {
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static Result<ClientNotification> toClientNotificationModel(final ClientEventRecord record) {
 | 
				
			||||||
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final Integer type = record.getType();
 | 
				
			||||||
 | 
					            final BigDecimal numericValue = record.getNumericValue();
 | 
				
			||||||
 | 
					            return new ClientNotification(
 | 
				
			||||||
 | 
					                    record.getId(),
 | 
				
			||||||
 | 
					                    record.getClientConnectionId(),
 | 
				
			||||||
 | 
					                    (type != null) ? EventType.byId(type) : EventType.UNKNOWN,
 | 
				
			||||||
 | 
					                    record.getClientTime(),
 | 
				
			||||||
 | 
					                    record.getServerTime(),
 | 
				
			||||||
 | 
					                    (numericValue != null) ? numericValue.doubleValue() : null,
 | 
				
			||||||
 | 
					                    record.getText());
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static Result<ClientEvent> toDomainModel(final ClientEventRecord record) {
 | 
					    private static Result<ClientEvent> toDomainModel(final ClientEventRecord record) {
 | 
				
			||||||
        return Result.tryCatch(() -> {
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,8 +36,8 @@ public interface SEBClientInstructionService {
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param clientInstruction the ClientInstruction instance to register
 | 
					     * @param clientInstruction the ClientInstruction instance to register
 | 
				
			||||||
     * @return A Result refer to a void marker or to an error if happened */
 | 
					     * @return A Result refer to a void marker or to an error if happened */
 | 
				
			||||||
    default Result<Void> registerInstruction(final ClientInstruction clientInstructionn) {
 | 
					    default Result<Void> registerInstruction(final ClientInstruction clientInstruction) {
 | 
				
			||||||
        return registerInstruction(clientInstructionn, false);
 | 
					        return registerInstruction(clientInstruction, false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Used to register a SEB client instruction for one or more active client connections
 | 
					    /** Used to register a SEB client instruction for one or more active client connections
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,39 +10,28 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.springframework.cache.annotation.CacheEvict;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
				
			||||||
import org.springframework.cache.annotation.Cacheable;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
					 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Service to maintain SEB Client notifications. */
 | 
					/** Service to maintain SEB Client notifications. */
 | 
				
			||||||
public interface SEBClientNotificationService {
 | 
					public interface SEBClientNotificationService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static final String CACHE_CLIENT_NOTIFICATION = "LIENT_NOTIFICATION_CACHE";
 | 
					    public static final String CACHE_CLIENT_NOTIFICATION = "CLIENT_NOTIFICATION_CACHE";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Indicates whether the client connection with the specified identifier has any
 | 
					    /** Indicates whether the client connection with the specified identifier has any
 | 
				
			||||||
     * pending notification or not. Pending means a non-confirmed notification
 | 
					     * pending notification or not. Pending means a non-confirmed notification
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param clientConnectionId the client connection identifier
 | 
					     * @param clientConnectionId the client connection identifier
 | 
				
			||||||
     * @return true if there is any pending notification for the specified client connection */
 | 
					     * @return true if there is any pending notification for the specified client connection */
 | 
				
			||||||
    @Cacheable(
 | 
					 | 
				
			||||||
            cacheNames = CACHE_CLIENT_NOTIFICATION,
 | 
					 | 
				
			||||||
            key = "#clientConnectionId",
 | 
					 | 
				
			||||||
            condition = "#result != null && #result")
 | 
					 | 
				
			||||||
    Boolean hasAnyPendingNotification(Long clientConnectionId);
 | 
					    Boolean hasAnyPendingNotification(Long clientConnectionId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Result<List<ClientEvent>> getPendingNotifications(Long clientConnectionId);
 | 
					    Result<List<ClientNotification>> getPendingNotifications(Long clientConnectionId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @CacheEvict(
 | 
					    Result<ClientNotification> confirmPendingNotification(
 | 
				
			||||||
            cacheNames = CACHE_CLIENT_NOTIFICATION,
 | 
					            Long notificationId,
 | 
				
			||||||
            key = "#clientConnectionId")
 | 
					            Long examId,
 | 
				
			||||||
    Result<ClientEvent> confirmPendingNotification(Long notificationId, final Long clientConnectionId);
 | 
					            String connectionToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @CacheEvict(
 | 
					    void notifyNewNotification(final Long clientConnectionId);
 | 
				
			||||||
            cacheNames = CACHE_CLIENT_NOTIFICATION,
 | 
					 | 
				
			||||||
            key = "#clientConnectionId")
 | 
					 | 
				
			||||||
    default void notifyNewNotification(final Long clientConnectionId) {
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,15 +9,22 @@
 | 
				
			||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
 | 
					package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.Collections;
 | 
					import java.util.Collections;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.HashSet;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.springframework.context.annotation.Lazy;
 | 
					import org.springframework.context.annotation.Lazy;
 | 
				
			||||||
import org.springframework.stereotype.Service;
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
					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.ClientNotification;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 | 
					import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Lazy
 | 
					@Lazy
 | 
				
			||||||
| 
						 | 
					@ -25,27 +32,85 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificati
 | 
				
			||||||
@WebServiceProfile
 | 
					@WebServiceProfile
 | 
				
			||||||
public class SEBClientNotificationServiceImpl implements SEBClientNotificationService {
 | 
					public class SEBClientNotificationServiceImpl implements SEBClientNotificationService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private final ClientEventDAO clientEventDAO;
 | 
					    private static final String CONFIRM_INSTRUCTION_ATTR_ID = "id";
 | 
				
			||||||
 | 
					    private static final String CONFIRM_INSTRUCTION_ATTR_TYPE = "type";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final ClientEventDAO clientEventDAO;
 | 
				
			||||||
 | 
					    private final SEBClientInstructionService sebClientInstructionService;
 | 
				
			||||||
 | 
					    private final Set<Long> pendingNotifications;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public SEBClientNotificationServiceImpl(
 | 
				
			||||||
 | 
					            final ClientEventDAO clientEventDAO,
 | 
				
			||||||
 | 
					            final SEBClientInstructionService sebClientInstructionService) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public SEBClientNotificationServiceImpl(final ClientEventDAO clientEventDAO) {
 | 
					 | 
				
			||||||
        this.clientEventDAO = clientEventDAO;
 | 
					        this.clientEventDAO = clientEventDAO;
 | 
				
			||||||
 | 
					        this.sebClientInstructionService = sebClientInstructionService;
 | 
				
			||||||
 | 
					        this.pendingNotifications = new HashSet<>();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public Boolean hasAnyPendingNotification(final Long clientConnectionId) {
 | 
					    public Boolean hasAnyPendingNotification(final Long clientConnectionId) {
 | 
				
			||||||
        return !getPendingNotifications(clientConnectionId)
 | 
					        if (this.pendingNotifications.contains(clientConnectionId)) {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final boolean hasAnyPendingNotification = !getPendingNotifications(clientConnectionId)
 | 
				
			||||||
                .getOr(Collections.emptyList())
 | 
					                .getOr(Collections.emptyList())
 | 
				
			||||||
                .isEmpty();
 | 
					                .isEmpty();
 | 
				
			||||||
 | 
					        if (hasAnyPendingNotification) {
 | 
				
			||||||
 | 
					            this.pendingNotifications.add(clientConnectionId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return hasAnyPendingNotification;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public Result<List<ClientEvent>> getPendingNotifications(final Long clientConnectionId) {
 | 
					    public Result<List<ClientNotification>> getPendingNotifications(final Long clientConnectionId) {
 | 
				
			||||||
        return this.clientEventDAO.getPendingNotifications(clientConnectionId);
 | 
					        return this.clientEventDAO.getPendingNotifications(clientConnectionId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public Result<ClientEvent> confirmPendingNotification(final Long notificatioId, final Long clientConnectionId) {
 | 
					    public Result<ClientNotification> confirmPendingNotification(
 | 
				
			||||||
        return this.clientEventDAO.confirmPendingNotification(notificatioId, clientConnectionId);
 | 
					            final Long notificationId,
 | 
				
			||||||
 | 
					            final Long examId,
 | 
				
			||||||
 | 
					            final String connectionToken) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.clientEventDAO.getPendingNotification(notificationId)
 | 
				
			||||||
 | 
					                .map(notification -> this.confirmClientSide(notification, examId, connectionToken))
 | 
				
			||||||
 | 
					                .flatMap(notification -> this.clientEventDAO.confirmPendingNotification(
 | 
				
			||||||
 | 
					                        notificationId,
 | 
				
			||||||
 | 
					                        notification.connectionId))
 | 
				
			||||||
 | 
					                .map(this::removeFromCache);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void notifyNewNotification(final Long clientConnectionId) {
 | 
				
			||||||
 | 
					        this.pendingNotifications.add(clientConnectionId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private ClientNotification confirmClientSide(
 | 
				
			||||||
 | 
					            final ClientNotification notification,
 | 
				
			||||||
 | 
					            final Long examId,
 | 
				
			||||||
 | 
					            final String connectionToken) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // create and send confirming SEB instruction to confirm the notification on client side
 | 
				
			||||||
 | 
					        final Map<String, String> attributes = new HashMap<>();
 | 
				
			||||||
 | 
					        attributes.put(CONFIRM_INSTRUCTION_ATTR_TYPE, notification.getNotificationType().typeName);
 | 
				
			||||||
 | 
					        attributes.put(CONFIRM_INSTRUCTION_ATTR_ID, String.valueOf((int) notification.getValue()));
 | 
				
			||||||
 | 
					        final ClientInstruction clientInstruction = new ClientInstruction(
 | 
				
			||||||
 | 
					                null,
 | 
				
			||||||
 | 
					                examId,
 | 
				
			||||||
 | 
					                InstructionType.NOTIFICATION_CONFIRM,
 | 
				
			||||||
 | 
					                connectionToken,
 | 
				
			||||||
 | 
					                attributes);
 | 
				
			||||||
 | 
					        this.sebClientInstructionService.registerInstruction(clientInstruction);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return notification;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private ClientNotification removeFromCache(final ClientNotification notification) {
 | 
				
			||||||
 | 
					        this.pendingNotifications.remove(notification.connectionId);
 | 
				
			||||||
 | 
					        return notification;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,8 +41,8 @@ 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.session.ClientConnection.ConnectionStatus;
 | 
					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.ClientConnectionData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
					 | 
				
			||||||
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.user.UserRole;
 | 
					import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 | 
					import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
 | 
				
			||||||
| 
						 | 
					@ -252,8 +252,9 @@ public class ExamMonitoringController {
 | 
				
			||||||
                    API.EXAM_MONITORING_NOTIFICATION_ENDPOINT +
 | 
					                    API.EXAM_MONITORING_NOTIFICATION_ENDPOINT +
 | 
				
			||||||
                    API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
 | 
					                    API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
 | 
				
			||||||
            method = RequestMethod.GET,
 | 
					            method = RequestMethod.GET,
 | 
				
			||||||
            consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
 | 
					            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
 | 
				
			||||||
    public Collection<ClientEvent> pendingNotifications(
 | 
					            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
 | 
				
			||||||
 | 
					    public Collection<ClientNotification> pendingNotifications(
 | 
				
			||||||
            @RequestParam(
 | 
					            @RequestParam(
 | 
				
			||||||
                    name = API.PARAM_INSTITUTION_ID,
 | 
					                    name = API.PARAM_INSTITUTION_ID,
 | 
				
			||||||
                    required = true,
 | 
					                    required = true,
 | 
				
			||||||
| 
						 | 
					@ -276,7 +277,7 @@ public class ExamMonitoringController {
 | 
				
			||||||
                    API.MODEL_ID_VAR_PATH_SEGMENT +
 | 
					                    API.MODEL_ID_VAR_PATH_SEGMENT +
 | 
				
			||||||
                    API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
 | 
					                    API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
 | 
				
			||||||
            method = RequestMethod.POST,
 | 
					            method = RequestMethod.POST,
 | 
				
			||||||
            consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
 | 
					            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
 | 
				
			||||||
    public void confirmNotification(
 | 
					    public void confirmNotification(
 | 
				
			||||||
            @RequestParam(
 | 
					            @RequestParam(
 | 
				
			||||||
                    name = API.PARAM_INSTITUTION_ID,
 | 
					                    name = API.PARAM_INSTITUTION_ID,
 | 
				
			||||||
| 
						 | 
					@ -286,13 +287,10 @@ public class ExamMonitoringController {
 | 
				
			||||||
            @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long notificationId,
 | 
					            @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long notificationId,
 | 
				
			||||||
            @PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) {
 | 
					            @PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final ClientConnectionData connection = getConnectionDataForSingleConnection(
 | 
					 | 
				
			||||||
                institutionId,
 | 
					 | 
				
			||||||
                examId,
 | 
					 | 
				
			||||||
                connectionToken);
 | 
					 | 
				
			||||||
        this.sebClientNotificationService.confirmPendingNotification(
 | 
					        this.sebClientNotificationService.confirmPendingNotification(
 | 
				
			||||||
                notificationId,
 | 
					                notificationId,
 | 
				
			||||||
                connection.getConnectionId())
 | 
					                examId,
 | 
				
			||||||
 | 
					                connectionToken)
 | 
				
			||||||
                .getOrThrow();
 | 
					                .getOrThrow();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1522,9 +1522,12 @@ sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined
 | 
				
			||||||
sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined
 | 
					sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined
 | 
				
			||||||
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.notificationlist.actions=
 | 
				
			||||||
sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification
 | 
					sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification
 | 
				
			||||||
sebserver.monitoring.exam.connection.action.confirm.notification.text=Are you sure you want to confirm this pending notification?<br/><br/>Note that this will send a notification confirmation instruction to the SEB client and remove this notification from the pending list.
 | 
					sebserver.monitoring.exam.connection.action.confirm.notification.text=Are you sure you want to confirm this pending notification?<br/><br/>Note that this will send a notification confirmation instruction to the SEB client and remove this notification from the pending list.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sebserver.monitoring.exam.connection.notificationlist.pleaseSelect=At first please select a notification form the Pending Notification list
 | 
				
			||||||
sebserver.monitoring.exam.connection.notificationlist.title=Pending Notification
 | 
					sebserver.monitoring.exam.connection.notificationlist.title=Pending Notification
 | 
				
			||||||
sebserver.monitoring.exam.connection.notificationlist.title.tooltip=All pending notifications sent by the SEB Client
 | 
					sebserver.monitoring.exam.connection.notificationlist.title.tooltip=All pending notifications sent by the SEB Client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1549,6 +1552,13 @@ sebserver.monitoring.exam.connection.event.type.INFO_LOG=Info
 | 
				
			||||||
sebserver.monitoring.exam.connection.event.type.WARN_LOG=Warn
 | 
					sebserver.monitoring.exam.connection.event.type.WARN_LOG=Warn
 | 
				
			||||||
sebserver.monitoring.exam.connection.event.type.ERROR_LOG=Error
 | 
					sebserver.monitoring.exam.connection.event.type.ERROR_LOG=Error
 | 
				
			||||||
sebserver.monitoring.exam.connection.event.type.LAST_PING=Last Ping
 | 
					sebserver.monitoring.exam.connection.event.type.LAST_PING=Last Ping
 | 
				
			||||||
 | 
					sebserver.monitoring.exam.connection.event.type.NOTIFICATION=Notification (pending)
 | 
				
			||||||
 | 
					sebserver.monitoring.exam.connection.event.type.NOTIFICATION_CONFIRM=Notification (confirmed)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sebserver.monitoring.exam.connection.notification.type.UNKNOWN=Unknown
 | 
				
			||||||
 | 
					sebserver.monitoring.exam.connection.notification.type.LOCK_SCREEN=Lock Screen
 | 
				
			||||||
 | 
					sebserver.monitoring.exam.connection.notification.type.RAISE_HAND=Raise Hand
 | 
				
			||||||
 | 
					sebserver.monitoring.exam.connection.notificationlist.type=Notification Type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sebserver.monitoring.exam.connection.status.UNDEFINED=Undefined
 | 
					sebserver.monitoring.exam.connection.status.UNDEFINED=Undefined
 | 
				
			||||||
sebserver.monitoring.exam.connection.status.CONNECTION_REQUESTED=Connection Requested
 | 
					sebserver.monitoring.exam.connection.status.CONNECTION_REQUESTED=Connection Requested
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -244,6 +244,7 @@ public class ModelObjectJSONGenerator {
 | 
				
			||||||
        System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
 | 
					        System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        domainObject = new ClientConnectionData(
 | 
					        domainObject = new ClientConnectionData(
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
                false,
 | 
					                false,
 | 
				
			||||||
                new ClientConnection(
 | 
					                new ClientConnection(
 | 
				
			||||||
                        1L, 1L, 1L, ConnectionStatus.ACTIVE, UUID.randomUUID().toString(),
 | 
					                        1L, 1L, 1L, ConnectionStatus.ACTIVE, UUID.randomUUID().toString(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue