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;
|
||||
|
||||
@JsonProperty(INDICATOR.ATTR_TAGS)
|
||||
@Size(min = 3, max = 255, message = "indicator:tag:size:{min}:{max}:${validatedValue}")
|
||||
public final String tags;
|
||||
|
||||
@JsonProperty(THRESHOLD.REFERENCE_NAME)
|
||||
|
|
|
@ -33,14 +33,17 @@ public class ClientConnectionData {
|
|||
public final List<? extends IndicatorValue> indicatorValues;
|
||||
|
||||
public final Boolean missingPing;
|
||||
public final Boolean pendingNotification;
|
||||
|
||||
@JsonCreator
|
||||
public ClientConnectionData(
|
||||
@JsonProperty(ATTR_MISSING_PING) final Boolean missingPing,
|
||||
@JsonProperty(ATTR_PENDING_NOTIFICATION) final Boolean pendingNotification,
|
||||
@JsonProperty(ATTR_CLIENT_CONNECTION) final ClientConnection clientConnection,
|
||||
@JsonProperty(ATTR_INDICATOR_VALUE) final Collection<? extends SimpleIndicatorValue> indicatorValues) {
|
||||
|
||||
this.missingPing = missingPing;
|
||||
this.pendingNotification = pendingNotification;
|
||||
this.clientConnection = clientConnection;
|
||||
this.indicatorValues = Utils.immutableListOf(indicatorValues);
|
||||
}
|
||||
|
@ -50,6 +53,7 @@ public class ClientConnectionData {
|
|||
final List<? extends IndicatorValue> indicatorValues) {
|
||||
|
||||
this.missingPing = null;
|
||||
this.pendingNotification = Boolean.FALSE;
|
||||
this.clientConnection = clientConnection;
|
||||
this.indicatorValues = Utils.immutableListOf(indicatorValues);
|
||||
}
|
||||
|
@ -61,7 +65,7 @@ public class ClientConnectionData {
|
|||
|
||||
@JsonProperty(ATTR_PENDING_NOTIFICATION)
|
||||
public Boolean pendingNotification() {
|
||||
return false;
|
||||
return this.pendingNotification;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
|
|
|
@ -26,7 +26,8 @@ public final class ClientInstruction {
|
|||
public enum InstructionType {
|
||||
SEB_QUIT,
|
||||
SEB_PROCTORING,
|
||||
SEB_RECONFIGURE_SETTINGS
|
||||
SEB_RECONFIGURE_SETTINGS,
|
||||
NOTIFICATION_CONFIRM
|
||||
}
|
||||
|
||||
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.ClientConnectionData;
|
||||
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.RemoteProctoringRoom;
|
||||
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 =
|
||||
new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.title.tooltip");
|
||||
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 =
|
||||
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");
|
||||
private static final LocTextKey LIST_COLUMN_TYPE_KEY =
|
||||
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.type");
|
||||
|
||||
private static final LocTextKey LIST_COLUMN_CLIENT_TIME_KEY =
|
||||
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.clienttime");
|
||||
private static final LocTextKey LIST_COLUMN_SERVER_TIME_KEY =
|
||||
|
@ -212,6 +218,7 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
.clearAttributes()
|
||||
.clearEntityKeys());
|
||||
|
||||
// NOTIFICATIONS
|
||||
final boolean hasNotification = BooleanUtils.isTrue(connectionData.pendingNotification());
|
||||
if (hasNotification) {
|
||||
// add notification table
|
||||
|
@ -221,7 +228,7 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
NOTIFICATION_LIST_TITLE_KEY,
|
||||
NOTIFICATION_LIST_TITLE_TOOLTIP_KEY);
|
||||
|
||||
final EntityTable<ClientEvent> notificationTable = this.pageService.remoteListTableBuilder(
|
||||
final EntityTable<ClientNotification> notificationTable = this.pageService.remoteListTableBuilder(
|
||||
restService.getRestCall(GetPendingClientNotifications.class),
|
||||
EntityType.CLIENT_EVENT)
|
||||
.withRestCallAdapter(builder -> builder.withURIVariable(
|
||||
|
@ -230,22 +237,22 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
.withURIVariable(
|
||||
API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
connectionToken))
|
||||
.withPaging(5)
|
||||
.withColumn(new ColumnDefinition<ClientEvent>(
|
||||
Domain.CLIENT_EVENT.ATTR_TYPE,
|
||||
LIST_COLUMN_TYPE_KEY,
|
||||
this.resourceService::getEventTypeName)
|
||||
.withPaging(-1)
|
||||
.withColumn(new ColumnDefinition<ClientNotification>(
|
||||
ClientNotification.ATTR_NOTIFICATION_TYPE,
|
||||
NOTIFICATION_LIST_COLUMN_TYPE_KEY,
|
||||
this.resourceService::getNotificationTypeName)
|
||||
.sortable()
|
||||
.widthProportion(2))
|
||||
|
||||
.withColumn(new ColumnDefinition<>(
|
||||
.withColumn(new ColumnDefinition<ClientNotification>(
|
||||
Domain.CLIENT_EVENT.ATTR_TEXT,
|
||||
LIST_COLUMN_TEXT_KEY,
|
||||
ClientEvent::getText)
|
||||
.sortable()
|
||||
.withCellTooltip()
|
||||
.widthProportion(4))
|
||||
.withColumn(new ColumnDefinition<>(
|
||||
.withColumn(new ColumnDefinition<ClientNotification>(
|
||||
Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
|
||||
new LocTextKey(LIST_COLUMN_SERVER_TIME_KEY.name,
|
||||
this.i18nSupport.getUsersTimeZoneTitleSuffix()),
|
||||
|
@ -268,10 +275,15 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION)
|
||||
.withParentEntityKey(parentEntityKey)
|
||||
.withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY)
|
||||
.withExec(action -> this.confirmNotification(action, connectionData))
|
||||
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER));
|
||||
.withSelect(
|
||||
() -> notificationTable.getSelection(),
|
||||
action -> this.confirmNotification(action, connectionData),
|
||||
NOTIFICATION_LIST_NO_SELECTION_KEY)
|
||||
|
||||
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false);
|
||||
}
|
||||
|
||||
// CLIENT EVENTS
|
||||
widgetFactory.addFormSubContextHeader(
|
||||
content,
|
||||
EVENT_LIST_TITLE_KEY,
|
||||
|
@ -401,7 +413,13 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
.call()
|
||||
.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(
|
||||
|
|
|
@ -25,6 +25,8 @@ public enum ActionCategory {
|
|||
SEB_CONFIG_TEMPLATE_LIST(new LocTextKey("sebserver.configtemplate.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),
|
||||
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),
|
||||
LOGS_USER_ACTIVITY_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"),
|
||||
ImageIcon.YES,
|
||||
PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
|
||||
ActionCategory.FORM),
|
||||
ActionCategory.EXAM_MONITORING_NOTIFICATION_LIST),
|
||||
|
||||
MONITOR_EXAM_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.ClientEvent;
|
||||
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.UserInfo;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType;
|
||||
|
@ -118,6 +120,8 @@ public class ResourceService {
|
|||
AttributeType.INLINE_TABLE);
|
||||
|
||||
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 ENTITY_TYPE_PREFIX = "sebserver.overall.types.entityType.";
|
||||
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());
|
||||
}
|
||||
|
||||
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() {
|
||||
return Arrays.stream(IndicatorType.values())
|
||||
.map(type -> new Tuple3<>(
|
||||
|
|
|
@ -339,6 +339,7 @@ public class PageContextImpl implements PageContext {
|
|||
this.i18nSupport);
|
||||
messageBox.setMarkupEnabled(true);
|
||||
messageBox.open(null);
|
||||
log.error("Unexpected error on GUI: ", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -32,7 +32,7 @@ public class ConfirmPendingClientNotification extends RestCall<Void> {
|
|||
new TypeReference<Void>() {
|
||||
}),
|
||||
HttpMethod.POST,
|
||||
MediaType.APPLICATION_JSON_UTF8,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_MONITORING_ENDPOINT
|
||||
+ API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ 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.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.gui.service.remote.webservice.api.RestCall;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
@GuiProfile
|
||||
public class GetPendingClientNotifications extends RestCall<Collection<ClientEvent>> {
|
||||
public class GetPendingClientNotifications extends RestCall<Collection<ClientNotification>> {
|
||||
|
||||
public GetPendingClientNotifications() {
|
||||
super(new TypeKey<>(
|
||||
CallType.GET_LIST,
|
||||
EntityType.CLIENT_EVENT,
|
||||
new TypeReference<Collection<ClientEvent>>() {
|
||||
new TypeReference<Collection<ClientNotification>>() {
|
||||
}),
|
||||
HttpMethod.GET,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
|
|
|
@ -513,13 +513,9 @@ public final class ClientConnectionTable {
|
|||
if (list != null && list.size() > 1) {
|
||||
tableItem.setBackground(0, ClientConnectionTable.this.colorData.color3);
|
||||
tableItem.setForeground(0, ClientConnectionTable.this.lightFontColor);
|
||||
tableItem.setImage(1,
|
||||
WidgetFactory.ImageIcon.ADD.getImage(ClientConnectionTable.this.table.getDisplay()));
|
||||
} else {
|
||||
tableItem.setBackground(0, null);
|
||||
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.navigator = new TableNavigator(this);
|
||||
this.navigator = (pageSize > 0) ? new TableNavigator(this) : new TableNavigator();
|
||||
|
||||
createTableColumns();
|
||||
this.pageNumber = initCurrentPageFromUserAttr();
|
||||
|
|
|
@ -108,6 +108,10 @@ public class RemoteListPageSupplier<T> implements PageSupplier<T> {
|
|||
}
|
||||
|
||||
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,
|
||||
this.pageNumber * this.pageSize + this.pageSize);
|
||||
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;
|
||||
|
||||
if (numOfPages <= 0) {
|
||||
return new Page<>(1, 1, this.column, this.list);
|
||||
}
|
||||
final List<T> subList = this.list.subList(this.pageNumber * this.pageSize,
|
||||
this.pageNumber * this.pageSize + this.pageSize);
|
||||
return new Page<>(numOfPages, this.pageNumber, this.column, subList);
|
||||
|
|
|
@ -26,6 +26,11 @@ public class TableNavigator {
|
|||
private final Composite composite;
|
||||
private final EntityTable<?> entityTable;
|
||||
|
||||
TableNavigator() {
|
||||
this.composite = null;
|
||||
this.entityTable = null;
|
||||
}
|
||||
|
||||
TableNavigator(final EntityTable<?> entityTable) {
|
||||
this.composite = new Composite(entityTable.composite, SWT.NONE);
|
||||
final GridData gridData = new GridData(SWT.LEFT, SWT.CENTER, true, true);
|
||||
|
@ -37,6 +42,10 @@ public class TableNavigator {
|
|||
}
|
||||
|
||||
public Page<?> update(final Page<?> pageData) {
|
||||
if (this.composite == null) {
|
||||
return pageData;
|
||||
}
|
||||
|
||||
// clear all
|
||||
PageService.clearComposite(this.composite);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import java.util.List;
|
|||
import java.util.function.Predicate;
|
||||
|
||||
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.util.Result;
|
||||
|
||||
|
@ -27,8 +28,10 @@ public interface ClientEventDAO extends EntityDAO<ClientEvent, ClientEvent> {
|
|||
FilterMap filterMap,
|
||||
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.ClientEvent;
|
||||
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.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
|
@ -187,21 +188,31 @@ public class ClientEventDAOImpl implements ClientEventDAO {
|
|||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<List<ClientEvent>> getPendingNotifications(final Long clientConnectionId) {
|
||||
return Result.tryCatch(() -> this.clientEventRecordMapper.selectByExample()
|
||||
public Result<ClientNotification> getPendingNotification(final Long notificationId) {
|
||||
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))
|
||||
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(EventType.NOTIFICATION.id))
|
||||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.map(ClientEventDAOImpl::toDomainModel)
|
||||
.map(ClientEventDAOImpl::toClientNotificationModel)
|
||||
.flatMap(DAOLoggingSupport::logAndSkipOnError)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Result<ClientEvent> confirmPendingNotification(final Long notificationId, final Long clientConnectionId) {
|
||||
public Result<ClientNotification> confirmPendingNotification(final Long notificationId,
|
||||
final Long clientConnectionId) {
|
||||
return Result.tryCatch(() -> {
|
||||
final Long pk = this.clientEventRecordMapper.selectIdsByExample()
|
||||
.where(ClientEventRecordDynamicSqlSupport.id, isEqualTo(notificationId))
|
||||
|
@ -218,7 +229,7 @@ public class ClientEventDAOImpl implements ClientEventDAO {
|
|||
|
||||
return this.clientEventRecordMapper.selectByPrimaryKey(pk);
|
||||
})
|
||||
.flatMap(ClientEventDAOImpl::toDomainModel)
|
||||
.flatMap(ClientEventDAOImpl::toClientNotificationModel)
|
||||
.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) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
|
|
|
@ -36,8 +36,8 @@ public interface SEBClientInstructionService {
|
|||
*
|
||||
* @param clientInstruction the ClientInstruction instance to register
|
||||
* @return A Result refer to a void marker or to an error if happened */
|
||||
default Result<Void> registerInstruction(final ClientInstruction clientInstructionn) {
|
||||
return registerInstruction(clientInstructionn, false);
|
||||
default Result<Void> registerInstruction(final ClientInstruction clientInstruction) {
|
||||
return registerInstruction(clientInstruction, false);
|
||||
}
|
||||
|
||||
/** 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 org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
|
||||
/** Service to maintain SEB Client notifications. */
|
||||
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
|
||||
* pending notification or not. Pending means a non-confirmed notification
|
||||
*
|
||||
* @param clientConnectionId the client connection identifier
|
||||
* @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);
|
||||
|
||||
Result<List<ClientEvent>> getPendingNotifications(Long clientConnectionId);
|
||||
Result<List<ClientNotification>> getPendingNotifications(Long clientConnectionId);
|
||||
|
||||
@CacheEvict(
|
||||
cacheNames = CACHE_CLIENT_NOTIFICATION,
|
||||
key = "#clientConnectionId")
|
||||
Result<ClientEvent> confirmPendingNotification(Long notificationId, final Long clientConnectionId);
|
||||
Result<ClientNotification> confirmPendingNotification(
|
||||
Long notificationId,
|
||||
Long examId,
|
||||
String connectionToken);
|
||||
|
||||
@CacheEvict(
|
||||
cacheNames = CACHE_CLIENT_NOTIFICATION,
|
||||
key = "#clientConnectionId")
|
||||
default void notifyNewNotification(final Long clientConnectionId) {
|
||||
}
|
||||
void notifyNewNotification(final Long clientConnectionId);
|
||||
|
||||
}
|
||||
|
|
|
@ -9,15 +9,22 @@
|
|||
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
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.util.Result;
|
||||
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;
|
||||
|
||||
@Lazy
|
||||
|
@ -25,27 +32,85 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificati
|
|||
@WebServiceProfile
|
||||
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.sebClientInstructionService = sebClientInstructionService;
|
||||
this.pendingNotifications = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean hasAnyPendingNotification(final Long clientConnectionId) {
|
||||
return !getPendingNotifications(clientConnectionId)
|
||||
if (this.pendingNotifications.contains(clientConnectionId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final boolean hasAnyPendingNotification = !getPendingNotifications(clientConnectionId)
|
||||
.getOr(Collections.emptyList())
|
||||
.isEmpty();
|
||||
if (hasAnyPendingNotification) {
|
||||
this.pendingNotifications.add(clientConnectionId);
|
||||
}
|
||||
|
||||
return hasAnyPendingNotification;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<ClientEvent>> getPendingNotifications(final Long clientConnectionId) {
|
||||
public Result<List<ClientNotification>> getPendingNotifications(final Long clientConnectionId) {
|
||||
return this.clientEventDAO.getPendingNotifications(clientConnectionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<ClientEvent> confirmPendingNotification(final Long notificatioId, final Long clientConnectionId) {
|
||||
return this.clientEventDAO.confirmPendingNotification(notificatioId, clientConnectionId);
|
||||
public Result<ClientNotification> confirmPendingNotification(
|
||||
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.session.ClientConnection.ConnectionStatus;
|
||||
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.ClientNotification;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
|
||||
|
@ -252,8 +252,9 @@ public class ExamMonitoringController {
|
|||
API.EXAM_MONITORING_NOTIFICATION_ENDPOINT +
|
||||
API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public Collection<ClientEvent> pendingNotifications(
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public Collection<ClientNotification> pendingNotifications(
|
||||
@RequestParam(
|
||||
name = API.PARAM_INSTITUTION_ID,
|
||||
required = true,
|
||||
|
@ -276,7 +277,7 @@ public class ExamMonitoringController {
|
|||
API.MODEL_ID_VAR_PATH_SEGMENT +
|
||||
API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public void confirmNotification(
|
||||
@RequestParam(
|
||||
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.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) {
|
||||
|
||||
final ClientConnectionData connection = getConnectionDataForSingleConnection(
|
||||
institutionId,
|
||||
examId,
|
||||
connectionToken);
|
||||
this.sebClientNotificationService.confirmPendingNotification(
|
||||
notificationId,
|
||||
connection.getConnectionId())
|
||||
examId,
|
||||
connectionToken)
|
||||
.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.proctoring=Single 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.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.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.ERROR_LOG=Error
|
||||
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.CONNECTION_REQUESTED=Connection Requested
|
||||
|
|
|
@ -244,6 +244,7 @@ public class ModelObjectJSONGenerator {
|
|||
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
|
||||
|
||||
domainObject = new ClientConnectionData(
|
||||
false,
|
||||
false,
|
||||
new ClientConnection(
|
||||
1L, 1L, 1L, ConnectionStatus.ACTIVE, UUID.randomUUID().toString(),
|
||||
|
|
Loading…
Add table
Reference in a new issue