diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java index dc65b37a..a43e7149 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringClientConnection.java @@ -13,6 +13,7 @@ import java.util.Base64.Encoder; import java.util.Collection; import java.util.Optional; +import org.apache.commons.lang3.BooleanUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; import org.eclipse.swt.widgets.Composite; @@ -57,7 +58,9 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.ConfirmPendingClientNotification; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetPendingClientNotifications; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProcotringRooms; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnectionData; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; @@ -66,6 +69,7 @@ import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService; import ch.ethz.seb.sebserver.gui.table.ColumnDefinition; import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute; +import ch.ethz.seb.sebserver.gui.table.EntityTable; import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; @@ -76,19 +80,16 @@ public class MonitoringClientConnection implements TemplateComposer { private static final Logger log = LoggerFactory.getLogger(MonitoringClientConnection.class); -// // @formatter:off -// private static final String OPEN_SINGEL_ROOM_SCRIPT = -// "var existingWin = window.open('', '%s', 'height=420,width=640,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes,dialog=yes');\n" + -// "if(existingWin.location.href === 'about:blank'){\n" + -// " existingWin.location.href = '%s%s';\n" + -// " existingWin.focus();\n" + -// "} else {\n" + -// " existingWin.focus();\n" + -// "}"; -// // @formatter:on - private static final LocTextKey PAGE_TITLE_KEY = new LocTextKey("sebserver.monitoring.exam.connection.title"); + + private static final LocTextKey NOTIFICATION_LIST_TITLE_KEY = + new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.title"); + 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"); + private static final LocTextKey EVENT_LIST_TITLE_KEY = new LocTextKey("sebserver.monitoring.exam.connection.eventlist.title"); private static final LocTextKey EVENT_LIST_TITLE_TOOLTIP_KEY = @@ -205,17 +206,77 @@ public class MonitoringClientConnection implements TemplateComposer { context1 -> clientConnectionDetails.updateData(), context -> clientConnectionDetails.updateGUI()); - widgetFactory.addFormSubContextHeader( - content, - EVENT_LIST_TITLE_KEY, - EVENT_LIST_TITLE_TOOLTIP_KEY); - final PageService.PageActionBuilder actionBuilder = this.pageService .pageActionBuilder( pageContext .clearAttributes() .clearEntityKeys()); + final boolean hasNotification = BooleanUtils.isTrue(connectionData.pendingNotification()); + if (hasNotification) { + // add notification table + + widgetFactory.addFormSubContextHeader( + content, + NOTIFICATION_LIST_TITLE_KEY, + NOTIFICATION_LIST_TITLE_TOOLTIP_KEY); + + final EntityTable notificationTable = this.pageService.remoteListTableBuilder( + restService.getRestCall(GetPendingClientNotifications.class), + EntityType.CLIENT_EVENT) + .withRestCallAdapter(builder -> builder.withURIVariable( + API.PARAM_PARENT_MODEL_ID, + parentEntityKey.modelId) + .withURIVariable( + API.EXAM_API_SEB_CONNECTION_TOKEN, + connectionToken)) + .withPaging(5) + .withColumn(new ColumnDefinition( + Domain.CLIENT_EVENT.ATTR_TYPE, + LIST_COLUMN_TYPE_KEY, + this.resourceService::getEventTypeName) + .sortable() + .widthProportion(2)) + + .withColumn(new ColumnDefinition<>( + Domain.CLIENT_EVENT.ATTR_TEXT, + LIST_COLUMN_TEXT_KEY, + ClientEvent::getText) + .sortable() + .withCellTooltip() + .widthProportion(4)) + .withColumn(new ColumnDefinition<>( + Domain.CLIENT_EVENT.ATTR_SERVER_TIME, + new LocTextKey(LIST_COLUMN_SERVER_TIME_KEY.name, + this.i18nSupport.getUsersTimeZoneTitleSuffix()), + this::getServerTime) + .sortable() + .widthProportion(1)) + .withDefaultAction(t -> actionBuilder + .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION) + .withParentEntityKey(parentEntityKey) + .withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY) + .withExec(action -> this.confirmNotification(action, connectionData)) + .noEventPropagation() + .create()) + .withSelectionListener(this.pageService.getSelectionPublisher( + pageContext, + ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION)) + .compose(pageContext.copyOf(content)); + + actionBuilder + .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)); + } + + widgetFactory.addFormSubContextHeader( + content, + EVENT_LIST_TITLE_KEY, + EVENT_LIST_TITLE_TOOLTIP_KEY); + // client event table for this connection this.pageService.entityTableBuilder(restService.getRestCall(GetExtendedClientEventPage.class)) .withEmptyMessage(EMPTY_LIST_TEXT_KEY) @@ -325,6 +386,24 @@ public class MonitoringClientConnection implements TemplateComposer { } } + private PageAction confirmNotification( + final PageAction pageAction, + final ClientConnectionData connectionData) { + + final EntityKey entityKey = pageAction.getSingleSelection(); + final EntityKey parentEntityKey = pageAction.getParentEntityKey(); + + this.pageService.getRestService() + .getBuilder(ConfirmPendingClientNotification.class) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, parentEntityKey.modelId) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionData.clientConnection.connectionToken) + .call() + .getOrThrow(); + + return pageAction; + } + private PageAction openExamCollectionProctorScreen( final PageAction action, final ClientConnectionData connectionData) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index f3578601..10bfc3d6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -640,6 +640,11 @@ public enum ActionDefinition { ImageIcon.PROCTOR_ROOM, PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION, ActionCategory.FORM), + MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION( + new LocTextKey("sebserver.monitoring.exam.connection.action.confirm.notification"), + ImageIcon.YES, + PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION, + ActionCategory.FORM), MONITOR_EXAM_QUIT_SELECTED( new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java index 66ddd1bb..0f4be109 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java @@ -9,6 +9,7 @@ package ch.ethz.seb.sebserver.gui.service.page; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -336,6 +337,8 @@ public interface PageService { TableBuilder staticListTableBuilder(final List staticList, EntityType entityType); + TableBuilder remoteListTableBuilder(RestCall> apiCall, EntityType entityType); + /** Get a new PageActionBuilder for a given PageContext. * * @param pageContext the PageContext that is used by the new PageActionBuilder diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java index ba725a6d..afc90d83 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java @@ -110,6 +110,10 @@ public final class PageAction { return this.pageContext.getEntityKey(); } + public EntityKey getParentEntityKey() { + return this.pageContext.getParentEntityKey(); + } + public PageContext pageContext() { return this.pageContext; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java index d8f14830..f991cf59 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageServiceImpl.java @@ -60,6 +60,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.table.EntityTable; +import ch.ethz.seb.sebserver.gui.table.RemoteListPageSupplier; import ch.ethz.seb.sebserver.gui.table.TableBuilder; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; @@ -383,6 +384,20 @@ public class PageServiceImpl implements PageService { this, staticList, entityType); } + @Override + public TableBuilder remoteListTableBuilder( + final RestCall> apiCall, + final EntityType entityType) { + + return new TableBuilder<>( + (entityType != null) + ? entityType.name() + : "", + this, + new RemoteListPageSupplier<>(apiCall, entityType), + entityType); + } + @Override public boolean logout(final PageContext pageContext) { this.clearState(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/ConfirmPendingClientNotification.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/ConfirmPendingClientNotification.java new file mode 100644 index 00000000..42e5375b --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/ConfirmPendingClientNotification.java @@ -0,0 +1,43 @@ +/* + * 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.gui.service.remote.webservice.api.session; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class ConfirmPendingClientNotification extends RestCall { + + public ConfirmPendingClientNotification() { + super(new TypeKey<>( + CallType.UNDEFINED, + EntityType.CLIENT_EVENT, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_JSON_UTF8, + API.EXAM_MONITORING_ENDPOINT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_NOTIFICATION_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetPendingClientNotifications.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetPendingClientNotifications.java new file mode 100644 index 00000000..daf9c114 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetPendingClientNotifications.java @@ -0,0 +1,45 @@ +/* + * 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.gui.service.remote.webservice.api.session; + +import java.util.Collection; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; +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> { + + public GetPendingClientNotifications() { + super(new TypeKey<>( + CallType.GET_LIST, + EntityType.CLIENT_EVENT, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_MONITORING_ENDPOINT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_NOTIFICATION_ENDPOINT + + API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/RemoteListPageSupplier.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/RemoteListPageSupplier.java new file mode 100644 index 00000000..e7a629cf --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/RemoteListPageSupplier.java @@ -0,0 +1,118 @@ +/* + * 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.gui.table; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import org.springframework.util.MultiValueMap; + +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Page; +import ch.ethz.seb.sebserver.gbl.model.PageSortOrder; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +public class RemoteListPageSupplier implements PageSupplier { + + private final EntityType entityType; + private final RestCall> restCall; + + public RemoteListPageSupplier(final RestCall> restCall, final EntityType entityType) { + this.restCall = restCall; + this.entityType = entityType; + } + + @Override + public EntityType getEntityType() { + return this.entityType; + } + + @Override + public Builder newBuilder() { + return new StaticListTableBuilderAdapter<>(this.restCall); + } + + public static final class StaticListTableBuilderAdapter implements Builder { + + private final RestCall>.RestCallBuilder restCallBuilder; + private int pageNumber; + private int pageSize; + private String column; + + private StaticListTableBuilderAdapter(final RestCall> restCall) { + this.restCallBuilder = restCall.newBuilder(); + } + + @Override + public Builder withPaging(final int pageNumber, final int pageSize) { + this.pageNumber = pageNumber; + this.pageSize = pageSize; + return this; + } + + @Override + public Builder withSorting(final String column, final PageSortOrder order) { + this.restCallBuilder.withSorting(column, order); + this.column = column; + return this; + } + + @Override + public Builder withQueryParams(final MultiValueMap params) { + this.restCallBuilder.withQueryParams(params); + return this; + } + + @Override + public Builder withQueryParam(final String name, final String value) { + this.restCallBuilder.withQueryParam(name, value); + return this; + } + + @Override + public Builder withURIVariable(final String name, final String id) { + this.restCallBuilder.withURIVariable(name, id); + return this; + } + + @Override + public Builder apply(final Function, Builder> f) { + return f.apply(this); + } + + @Override + public Result> getPage() { + return Result.tryCatch(() -> { + + final Collection collection = this.restCallBuilder.call().getOrThrow(); + final List list = (collection == null || collection.isEmpty()) + ? Collections.emptyList() + : new ArrayList<>(collection); + + if (list.isEmpty()) { + return new Page<>(0, this.pageNumber, this.column, list); + } + + if (this.pageSize <= 0) { + return new Page<>(1, 1, this.column, list); + } + + final int numOfPages = list.size() / this.pageSize; + final List subList = list.subList(this.pageNumber * this.pageSize, + this.pageNumber * this.pageSize + this.pageSize); + return new Page<>(numOfPages, this.pageNumber, this.column, subList); + }); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java index a5b4dd46..77ce5917 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableBuilder.java @@ -35,7 +35,7 @@ public class TableBuilder { private final String name; private final PageService pageService; final RestCall> restCall; - final List staticList; + final PageSupplier pageSupplier; final EntityType entityType; private final MultiValueMap staticQueryParams; final List> columns = new ArrayList<>(); @@ -58,7 +58,7 @@ public class TableBuilder { this.name = name; this.pageService = pageService; this.restCall = restCall; - this.staticList = null; + this.pageSupplier = null; this.entityType = null; this.staticQueryParams = new LinkedMultiValueMap<>(); } @@ -72,8 +72,22 @@ public class TableBuilder { this.name = name; this.pageService = pageService; this.restCall = null; - this.staticList = staticList; this.entityType = entityType; + this.pageSupplier = new StaticListPageSupplier<>(staticList, entityType); + this.staticQueryParams = new LinkedMultiValueMap<>(); + } + + public TableBuilder( + final String name, + final PageService pageService, + final PageSupplier pageSupplier, + final EntityType entityType) { + + this.name = name; + this.pageService = pageService; + this.restCall = null; + this.entityType = entityType; + this.pageSupplier = pageSupplier; this.staticQueryParams = new LinkedMultiValueMap<>(); } @@ -184,7 +198,7 @@ public class TableBuilder { pageContext, (this.restCall != null) ? new RestCallPageSupplier<>(this.restCall) - : new StaticListPageSupplier<>(this.staticList, this.entityType), + : this.pageSupplier, this.restCallAdapter, this.pageService, this.columns, diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index f6fd99f2..e4daf587 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1522,6 +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.action.confirm.notification=Confirm Notification +sebserver.monitoring.exam.connection.action.confirm.notification.text=Are you sure you want to confirm this pending notification?

Note that this will send a notification confirmation instruction to the SEB client and remove this notification from the pending list. + +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.eventlist.title=Events sebserver.monitoring.exam.connection.eventlist.title.tooltip=All events and logs sent by the SEB Client