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 45b1eabd..650531ab 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 @@ -8,14 +8,10 @@ package ch.ethz.seb.sebserver.gui.content; -import java.util.Arrays; import java.util.Collection; -import java.util.Optional; import java.util.function.Supplier; 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; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,18 +26,15 @@ import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; -import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; 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; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.gui.GuiServiceInfo; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.service.ResourceService; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; @@ -61,12 +54,11 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctorin 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.GetCollectingRooms; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetPendingClientNotifications; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails; import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; +import ch.ethz.seb.sebserver.gui.service.session.proctoring.MonitoringProctoringService; import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService; import ch.ethz.seb.sebserver.gui.table.ColumnDefinition; import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute; @@ -121,10 +113,9 @@ public class MonitoringClientConnection implements TemplateComposer { private final I18nSupport i18nSupport; private final InstructionProcessor instructionProcessor; private final SEBClientEventDetailsPopup sebClientLogDetailsPopup; - private final GuiServiceInfo guiServiceInfo; + private final MonitoringProctoringService monitoringProctoringService; private final long pollInterval; private final int pageSize; - private final String remoteProctoringEndpoint; private final TableFilterAttribute typeFilter; private final TableFilterAttribute textFilter = @@ -135,21 +126,19 @@ public class MonitoringClientConnection implements TemplateComposer { final PageService pageService, final InstructionProcessor instructionProcessor, final SEBClientEventDetailsPopup sebClientLogDetailsPopup, - final GuiServiceInfo guiServiceInfo, + final MonitoringProctoringService monitoringProctoringService, @Value("${sebserver.gui.webservice.poll-interval:500}") final long pollInterval, - @Value("${sebserver.gui.list.page.size:20}") final Integer pageSize, - @Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint) { + @Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) { this.serverPushService = serverPushService; this.pageService = pageService; this.resourceService = pageService.getResourceService(); this.i18nSupport = this.resourceService.getI18nSupport(); this.instructionProcessor = instructionProcessor; - this.guiServiceInfo = guiServiceInfo; + this.monitoringProctoringService = monitoringProctoringService; this.pollInterval = pollInterval; this.sebClientLogDetailsPopup = sebClientLogDetailsPopup; this.pageSize = pageSize; - this.remoteProctoringEndpoint = remoteProctoringEndpoint; this.typeFilter = new TableFilterAttribute( CriteriaType.SINGLE_SELECTION, @@ -380,20 +369,25 @@ public class MonitoringClientConnection implements TemplateComposer { .getBuilder(GetProctoringSettings.class) .withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId) .call() - .onError(error -> log.error("Failed to get ProctoringSettings", error)) + .onError(error -> log.error("Failed to get ProctoringServiceSettings", error)) .getOr(null); + final ProctoringGUIService proctoringGUIService = currentUser.getProctoringGUIService(); if (procotringSettings != null && procotringSettings.enableProctoring) { actionBuilder .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_PROCTORING) .withEntityKey(parentEntityKey) - .withExec(action -> this.openOneToOneRoom(action, connectionData)) + .withExec(action -> this.monitoringProctoringService.openOneToOneRoom( + action, + connectionData, proctoringGUIService)) .noEventPropagation() .publish() .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_EXAM_ROOM_PROCTORING) .withEntityKey(parentEntityKey) - .withExec(action -> this.openExamCollectionProctorScreen(action, connectionData)) + .withExec(action -> this.monitoringProctoringService.openExamCollectionProctorScreen( + action, + connectionData)) .noEventPropagation() .publish(); @@ -434,98 +428,6 @@ public class MonitoringClientConnection implements TemplateComposer { connectionData.clientConnection.connectionToken); } - private PageAction openExamCollectionProctorScreen( - final PageAction action, - final ClientConnectionData connectionData) { - - final String examId = action.getEntityKey().modelId; - - final ProctoringServiceSettings proctoringSettings = this.pageService.getRestService() - .getBuilder(GetProctoringSettings.class) - .withURIVariable(API.PARAM_MODEL_ID, examId) - .call() - .getOrThrow(); - - final Optional roomOptional = - this.pageService.getRestService().getBuilder(GetCollectingRooms.class) - .withURIVariable(API.PARAM_MODEL_ID, examId) - .call() - .getOrThrow() - .stream() - .filter(room -> room.id.equals(connectionData.clientConnection.remoteProctoringRoomId)) - .findFirst(); - - if (roomOptional.isPresent()) { - final RemoteProctoringRoom room = roomOptional.get(); - final ProctoringRoomConnection proctoringConnectionData = this.pageService - .getRestService() - .getBuilder(GetProctorRoomConnection.class) - .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId)) - .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name) - .withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject)) - .call() - .getOrThrow(); - - ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData); - final String script = String.format( - MonitoringRunningExam.OPEN_ROOM_SCRIPT, - room.name, - 800, - 1200, - this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), - this.remoteProctoringEndpoint); - - RWT.getClient() - .getService(JavaScriptExecutor.class) - .execute(script); - - this.pageService.getCurrentUser() - .getProctoringGUIService() - .registerProctoringWindow(examId, room.name, room.name); - } - - return action; - } - - private PageAction openOneToOneRoom( - final PageAction action, - final ClientConnectionData connectionData) { - - final String connectionToken = connectionData.clientConnection.connectionToken; - final String windowName = connectionToken + "_one2oneRooom"; - final String examId = action.getEntityKey().modelId; - - final ProctoringGUIService proctoringGUIService = this.pageService - .getCurrentUser() - .getProctoringGUIService(); - - if (!proctoringGUIService.hasWindow(windowName)) { - final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService - .openBreakOutRoom( - examId, - windowName, - connectionData.clientConnection.userSessionId, - Arrays.asList(connectionToken)) - .onError(error -> log.error( - "Failed to open single proctoring room for connection {} {}", - connectionToken, - error.getMessage())) - .getOr(null); - ProctoringGUIService.setCurrentProctoringWindowData(examId, windowName, proctoringConnectionData); - } - - final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class); - final String script = String.format( - MonitoringRunningExam.OPEN_ROOM_SCRIPT, - windowName, - 420, - 640, - this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), - this.remoteProctoringEndpoint); - javaScriptExecutor.execute(script); - return action; - } - private String getClientTime(final ClientEvent event) { if (event == null || event.getClientTime() == null) { return Constants.EMPTY_NOTE; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java index 43c06fbd..18404f01 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java @@ -10,33 +10,22 @@ package ch.ethz.seb.sebserver.gui.content; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.Set; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; -import org.apache.commons.lang3.BooleanUtils; -import org.eclipse.rap.rwt.RWT; -import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; import org.eclipse.swt.SWT; -import org.eclipse.swt.graphics.Color; -import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.MessageBox; -import org.eclipse.swt.widgets.Tree; -import org.eclipse.swt.widgets.TreeItem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.async.AsyncRunner; @@ -44,22 +33,16 @@ import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; -import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; -import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; -import ch.ethz.seb.sebserver.gbl.util.Pair; import ch.ethz.seb.sebserver.gbl.util.Tuple; import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.gui.GuiServiceInfo; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; -import ch.ethz.seb.sebserver.gui.content.action.ActionPane; import ch.ethz.seb.sebserver.gui.service.ResourceService; -import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageMessageException; @@ -76,12 +59,10 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; +import ch.ethz.seb.sebserver.gui.service.session.proctoring.MonitoringProctoringService; import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService; import ch.ethz.seb.sebserver.gui.widget.Message; @@ -92,23 +73,6 @@ public class MonitoringRunningExam implements TemplateComposer { private static final Logger log = LoggerFactory.getLogger(MonitoringRunningExam.class); - // @formatter:off - static final String OPEN_ROOM_SCRIPT = - "try {\n" + - "var existingWin = window.open('', '%s', 'height=%s,width=%s,location=no,scrollbars=yes,status=no,menubar=0,toolbar=no,titlebar=no,dialog=no');\n" + - "if(existingWin.location.href === 'about:blank'){\n" + - " existingWin.location.href = '%s%s';\n" + - " existingWin.focus();\n" + - "} else {\n" + - " existingWin.focus();\n" + - "}" + - "}\n" + - "catch(err) {\n" + - " alert(\"Unexpected Javascript Error happened: \" + err);\n"+ - "}"; - // @formatter:on - - private static final String SHOW_CONNECTION_ACTION_APPLIED = "SHOW_CONNECTION_ACTION_APPLIED"; private static final LocTextKey EMPTY_SELECTION_TEXT_KEY = new LocTextKey("sebserver.monitoring.exam.connection.emptySelection"); private static final LocTextKey EMPTY_ACTIVE_SELECTION_TEXT_KEY = @@ -119,8 +83,6 @@ public class MonitoringRunningExam implements TemplateComposer { new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm"); private static final LocTextKey CONFIRM_DISABLE_SELECTED = new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm"); - private static final LocTextKey EXAM_ROOM_NAME = - new LocTextKey("sebserver.monitoring.exam.proctoring.room.all.name"); private final ServerPushService serverPushService; private final PageService pageService; @@ -128,23 +90,19 @@ public class MonitoringRunningExam implements TemplateComposer { private final ResourceService resourceService; private final AsyncRunner asyncRunner; private final InstructionProcessor instructionProcessor; - private final GuiServiceInfo guiServiceInfo; private final MonitoringExamSearchPopup monitoringExamSearchPopup; - private final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup; + private final MonitoringProctoringService monitoringProctoringService; private final long pollInterval; private final long proctoringRoomUpdateInterval; - private final String remoteProctoringEndpoint; protected MonitoringRunningExam( final ServerPushService serverPushService, final PageService pageService, final AsyncRunner asyncRunner, final InstructionProcessor instructionProcessor, - final GuiServiceInfo guiServiceInfo, final MonitoringExamSearchPopup monitoringExamSearchPopup, - final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup, + final MonitoringProctoringService monitoringProctoringService, @Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval, - @Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint, @Value("${sebserver.gui.remote.proctoring.rooms.update.poll-interval:5000}") final long proctoringRoomUpdateInterval) { this.serverPushService = serverPushService; @@ -153,11 +111,9 @@ public class MonitoringRunningExam implements TemplateComposer { this.resourceService = pageService.getResourceService(); this.asyncRunner = asyncRunner; this.instructionProcessor = instructionProcessor; - this.guiServiceInfo = guiServiceInfo; + this.monitoringProctoringService = monitoringProctoringService; this.pollInterval = pollInterval; this.monitoringExamSearchPopup = monitoringExamSearchPopup; - this.remoteProctoringEndpoint = remoteProctoringEndpoint; - this.proctorRoomConnectionsPopup = proctorRoomConnectionsPopup; this.proctoringRoomUpdateInterval = proctoringRoomUpdateInterval; } @@ -308,11 +264,12 @@ public class MonitoringRunningExam implements TemplateComposer { actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) .withEntityKey(entityKey) - .withExec(action -> this.toggleTownhallRoom(proctoringGUIService, action)) + .withExec(action -> this.monitoringProctoringService.toggleTownhallRoom(proctoringGUIService, + action)) .noEventPropagation() .publish(); - if (isTownhallRoomActive(entityKey.modelId)) { + if (this.monitoringProctoringService.isTownhallRoomActive(entityKey.modelId)) { this.pageService.firePageEvent( new ActionActivationEvent( true, @@ -322,10 +279,8 @@ public class MonitoringRunningExam implements TemplateComposer { pageContext); } - final Map> availableRooms = new HashMap<>(); - updateRoomActions( + this.monitoringProctoringService.initCollectingRoomActions( pageContext, - availableRooms, actionBuilder, proctoringSettings, proctoringGUIService); @@ -335,9 +290,8 @@ public class MonitoringRunningExam implements TemplateComposer { Utils.truePredicate(), createServerPushUpdateErrorHandler(this.pageService, pageContext)), this.proctoringRoomUpdateInterval, - context -> updateRoomActions( + context -> this.monitoringProctoringService.updateCollectingRoomActions( pageContext, - availableRooms, actionBuilder, proctoringSettings, proctoringGUIService)); @@ -437,296 +391,11 @@ public class MonitoringRunningExam implements TemplateComposer { } } - private boolean isTownhallRoomActive(final String examModelId) { - return !BooleanUtils.toBoolean(this.pageService - .getRestService() - .getBuilder(IsTownhallRoomAvailable.class) - .withURIVariable(API.PARAM_MODEL_ID, examModelId) - .call() - .getOr(Constants.FALSE_STRING)); - } - private PageAction openSearchPopup(final PageAction action) { this.monitoringExamSearchPopup.show(action.pageContext()); return action; } - private PageAction toggleTownhallRoom( - final ProctoringGUIService proctoringGUIService, - final PageAction action) { - - if (isTownhallRoomActive(action.getEntityKey().modelId)) { - closeTownhallRoom(proctoringGUIService, action); - this.pageService.firePageEvent( - new ActionActivationEvent( - true, - new Tuple<>( - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)), - action.pageContext()); - return action; - } else { - openTownhallRoom(proctoringGUIService, action); - this.pageService.firePageEvent( - new ActionActivationEvent( - true, - new Tuple<>( - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), - action.pageContext()); - return action; - } - } - - private PageAction openTownhallRoom( - final ProctoringGUIService proctoringGUIService, - final PageAction action) { - - try { - final EntityKey examId = action.getEntityKey(); - - final String windowName = getTownhallWindowName(examId.modelId); - if (!proctoringGUIService.hasWindow(windowName)) { - final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService - .openTownhallRoom( - examId.modelId, - windowName, - this.pageService.getI18nSupport().getText(EXAM_ROOM_NAME)) - .onError(error -> log.error( - "Failed to open all collecting room for exam {} {}", examId.modelId, - error.getMessage())) - .getOrThrow(); - ProctoringGUIService.setCurrentProctoringWindowData( - examId.modelId, - windowName, - proctoringConnectionData); - } - - final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class); - final String script = String.format( - OPEN_ROOM_SCRIPT, - windowName, - 800, - 1200, - this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), - this.remoteProctoringEndpoint); - javaScriptExecutor.execute(script); - - } catch (final Exception e) { - log.error("Failed to open popup for town-hall room: ", e); - } - return action; - } - - private PageAction closeTownhallRoom( - final ProctoringGUIService proctoringGUIService, - final PageAction action) { - - final String examId = action.getEntityKey().modelId; - try { - - this.pageService - .getCurrentUser() - .getProctoringGUIService() - .closeRoomWindow(getTownhallWindowName(examId)); - - } catch (final Exception e) { - log.error("Failed to close proctoring town-hall room for exam: {}", examId); - } - return action; - } - - private void updateTownhallButton( - final ProctoringGUIService proctoringGUIService, - final PageContext pageContext) { - final EntityKey entityKey = pageContext.getEntityKey(); - - if (isTownhallRoomActive(entityKey.modelId)) { - final boolean townhallRoomFromThisUser = proctoringGUIService - .isTownhallOpenForUser(entityKey.modelId); - if (townhallRoomFromThisUser) { - this.pageService.firePageEvent( - new ActionActivationEvent( - true, - new Tuple<>( - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), - pageContext); - } else { - this.pageService.firePageEvent( - new ActionActivationEvent( - false, - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), - pageContext); - } - } else { - this.pageService.firePageEvent( - new ActionActivationEvent( - true, - new Tuple<>( - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM), - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), - pageContext); - } - } - - private void updateRoomActions( - final PageContext pageContext, - final Map> rooms, - final PageActionBuilder actionBuilder, - final ProctoringServiceSettings proctoringSettings, - final ProctoringGUIService proctoringGUIService) { - - final EntityKey entityKey = pageContext.getEntityKey(); - updateTownhallButton(proctoringGUIService, pageContext); - final I18nSupport i18nSupport = this.pageService.getI18nSupport(); - this.pageService - .getRestService() - .getBuilder(GetCollectingRooms.class) - .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) - .call() - .onError(error -> log.error("Failed to update proctoring rooms on GUI {}", error.getMessage())) - .getOr(Collections.emptyList()) - .stream() - .forEach(room -> { - if (rooms.containsKey(room.name)) { - // update action - final TreeItem treeItem = rooms.get(room.name).b; - rooms.put(room.name, new Pair<>(room, treeItem)); - treeItem.setText(i18nSupport.getText(new LocTextKey( - ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM.title.name, - room.subject, - room.roomSize, - proctoringSettings.collectingRoomSize))); - processProctorRoomActionActivation(treeItem, room, pageContext); - } else { - // create new action - final PageAction action = - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM) - .withEntityKey(entityKey) - .withExec(_action -> { - final int actualRoomSize = getActualRoomSize(room, rooms); - if (actualRoomSize <= 0) { - return _action; - } - return showExamProctoringRoom(proctoringSettings, room, _action); - }) - .withNameAttributes( - room.subject, - room.roomSize, - proctoringSettings.collectingRoomSize) - .noEventPropagation() - .create(); - - this.pageService.publishAction( - action, - _treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem))); - addRoomConnectionsPopupListener(pageContext, rooms); - processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext); - } - }); - } - - private void processProctorRoomActionActivation( - final TreeItem treeItem, - final RemoteProctoringRoom room, - final PageContext pageContext) { - - try { - final Display display = pageContext.getRoot().getDisplay(); - final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY); - final Image image = room.roomSize > 0 - ? action.definition.icon.getImage(display) - : action.definition.icon.getGreyedImage(display); - treeItem.setImage(image); - treeItem.setForeground(room.roomSize > 0 ? null : new Color(display, Constants.GREY_DISABLED)); - } catch (final Exception e) { - log.warn("Failed to set Proctor-Room-Activation: ", e.getMessage()); - } - } - - private void addRoomConnectionsPopupListener( - final PageContext pageContext, - final Map> rooms) { - - if (!rooms.isEmpty()) { - final EntityKey entityKey = pageContext.getEntityKey(); - final TreeItem treeItem = rooms.values().iterator().next().b; - final Tree tree = treeItem.getParent(); - if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) { - tree.addListener(SWT.Selection, event -> { - final TreeItem item = (TreeItem) event.item; - item.getParent().deselectAll(); - if (event.button == 3) { - rooms.entrySet() - .stream() - .filter(e -> e.getValue().b.equals(item)) - .findFirst() - .ifPresent(e -> { - final RemoteProctoringRoom room = e.getValue().a; - if (room.roomSize > 0) { - final PageContext pc = pageContext.copy() - .clearAttributes() - .withEntityKey(new EntityKey(room.name, - EntityType.REMOTE_PROCTORING_ROOM)) - .withParentEntityKey(entityKey); - this.proctorRoomConnectionsPopup.show(pc, room.subject); - } - }); - } - }); - tree.setData(SHOW_CONNECTION_ACTION_APPLIED, true); - } - } - } - - private int getActualRoomSize( - final RemoteProctoringRoom room, - final Map> rooms) { - - return rooms.get(room.name).a.roomSize; - } - - private PageAction showExamProctoringRoom( - final ProctoringServiceSettings proctoringSettings, - final RemoteProctoringRoom room, - final PageAction action) { - - final ProctoringRoomConnection proctoringConnectionData = this.pageService - .getRestService() - .getBuilder(GetProctorRoomConnection.class) - .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId)) - .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name) - .withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject)) - .call() - .getOrThrow(); - - ProctoringGUIService.setCurrentProctoringWindowData( - String.valueOf(proctoringSettings.examId), - proctoringConnectionData); - - final String script = String.format( - OPEN_ROOM_SCRIPT, - room.name, - 800, - 1200, - this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), - this.remoteProctoringEndpoint); - - RWT.getClient() - .getService(JavaScriptExecutor.class) - .execute(script); - - this.pageService.getCurrentUser() - .getProctoringGUIService() - .registerProctoringWindow(String.valueOf(room.examId), room.name, room.name); - - return action; - } - private static Function showStateViewAction( final ClientConnectionTable clientTable, final ConnectionStatus status) { @@ -835,8 +504,4 @@ public class MonitoringRunningExam implements TemplateComposer { }; } - private String getTownhallWindowName(final String examId) { - return examId + "_townhall"; - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/RemoteProctoringView.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/RemoteProctoringView.java index 2c1be80f..a3fc2396 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/RemoteProctoringView.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/RemoteProctoringView.java @@ -9,9 +9,25 @@ package ch.ethz.seb.sebserver.gui.service.page; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; public interface RemoteProctoringView extends TemplateComposer { + static final LocTextKey CLOSE_WINDOW_TEXT_KEY = + new LocTextKey("sebserver.monitoring.exam.proctoring.action.close"); + static final LocTextKey BROADCAST_AUDIO_ON_TEXT_KEY = + new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.audio"); + static final LocTextKey BROADCAST_AUDIO_OFF_TEXT_KEY = + new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.audio"); + static final LocTextKey BROADCAST_VIDEO_ON_TEXT_KEY = + new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.video"); + static final LocTextKey BROADCAST_VIDEO_OFF_TEXT_KEY = + new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.video"); + static final LocTextKey CHAT_ON_TEXT_KEY = + new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.chat"); + static final LocTextKey CHAT_OFF_TEXT_KEY = + new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.chat"); + /** Get the remote proctoring server type this remote proctoring view can handle. * * @return the remote proctoring server type this remote proctoring view can handle. */ diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/AbstractProctoringView.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/AbstractProctoringView.java new file mode 100644 index 00000000..3fb42ab3 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/AbstractProctoringView.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2021 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.page.impl; + +import org.eclipse.swt.widgets.Button; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gui.GuiServiceInfo; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.page.RemoteProctoringView; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendProctoringReconfigurationAttributes; +import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService; +import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData; + +public abstract class AbstractProctoringView implements RemoteProctoringView { + + private static final Logger log = LoggerFactory.getLogger(AbstractProctoringView.class); + + protected final PageService pageService; + protected final GuiServiceInfo guiServiceInfo; + protected final String remoteProctoringEndpoint; + protected final String remoteProctoringViewServletEndpoint; + + protected AbstractProctoringView( + final PageService pageService, + final GuiServiceInfo guiServiceInfo, + final String remoteProctoringEndpoint, + final String remoteProctoringViewServletEndpoint) { + + this.pageService = pageService; + this.guiServiceInfo = guiServiceInfo; + this.remoteProctoringEndpoint = remoteProctoringEndpoint; + this.remoteProctoringViewServletEndpoint = remoteProctoringViewServletEndpoint; + } + + protected void sendReconfigurationAttributes( + final String examId, + final String roomName, + final BroadcastActionState state) { + + this.pageService.getRestService().getBuilder(SendProctoringReconfigurationAttributes.class) + .withURIVariable(API.PARAM_MODEL_ID, examId) + .withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomName) + .withFormParam( + + API.EXAM_PROCTORING_ATTR_RECEIVE_AUDIO, + state.audio ? Constants.TRUE_STRING : Constants.FALSE_STRING) + .withFormParam( + API.EXAM_PROCTORING_ATTR_RECEIVE_VIDEO, + state.video ? Constants.TRUE_STRING : Constants.FALSE_STRING) + .withFormParam( + API.EXAM_PROCTORING_ATTR_ALLOW_CHAT, + state.chat ? Constants.TRUE_STRING : Constants.FALSE_STRING) + .call() + .onError(error -> log.error("Failed to send broadcast attributes to clients in room: {} cause: {}", + roomName, + error.getMessage())); + + } + + protected void toggleBroadcastAudio( + final String examId, + final String roomName, + final Button broadcastAction) { + + final BroadcastActionState state = + (BroadcastActionState) broadcastAction.getData(BroadcastActionState.KEY_NAME); + + this.pageService.getPolyglotPageService().injectI18n( + broadcastAction, + state.audio ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY); + + state.audio = !state.audio; + sendReconfigurationAttributes(examId, roomName, state); + } + + protected void toggleBroadcastVideo( + final String examId, + final String roomName, + final Button videoAction, + final Button audioAction) { + + final BroadcastActionState state = + (BroadcastActionState) videoAction.getData(BroadcastActionState.KEY_NAME); + + this.pageService.getPolyglotPageService().injectI18n( + audioAction, + state.video ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY); + this.pageService.getPolyglotPageService().injectI18n( + videoAction, + state.video ? BROADCAST_VIDEO_ON_TEXT_KEY : BROADCAST_VIDEO_OFF_TEXT_KEY); + + state.video = !state.video; + state.audio = state.video; + sendReconfigurationAttributes(examId, roomName, state); + } + + protected void toggleChat( + final String examId, + final String roomName, + final Button broadcastAction) { + + final BroadcastActionState state = + (BroadcastActionState) broadcastAction.getData(BroadcastActionState.KEY_NAME); + + this.pageService.getPolyglotPageService().injectI18n( + broadcastAction, + state.chat ? CHAT_ON_TEXT_KEY : CHAT_OFF_TEXT_KEY); + + state.chat = !state.chat; + sendReconfigurationAttributes(examId, roomName, state); + } + + protected void closeRoom( + final ProctoringGUIService proctoringGUIService, + final ProctoringWindowData proctoringWindowData) { + + try { + proctoringGUIService.closeRoomWindow(proctoringWindowData.windowName); + } catch (final Exception e) { + log.error("Failed to close proctoring window properly: ", e); + } + } + + static final class BroadcastActionState { + public static final String KEY_NAME = "BroadcastActionState"; + boolean audio = false; + boolean video = false; + boolean chat = false; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java index 629687c9..b52cb82a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java @@ -23,56 +23,28 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.api.API; -import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gui.GuiServiceInfo; -import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageService; -import ch.ethz.seb.sebserver.gui.service.page.RemoteProctoringView; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendProctoringReconfigurationAttributes; import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService; import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; @Component @GuiProfile -public class JitsiMeetProctoringView implements RemoteProctoringView { +public class JitsiMeetProctoringView extends AbstractProctoringView { private static final Logger log = LoggerFactory.getLogger(JitsiMeetProctoringView.class); - private static final LocTextKey CLOSE_WINDOW_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.close"); - private static final LocTextKey BROADCAST_AUDIO_ON_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.audio"); - private static final LocTextKey BROADCAST_AUDIO_OFF_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.audio"); - private static final LocTextKey BROADCAST_VIDEO_ON_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.video"); - private static final LocTextKey BROADCAST_VIDEO_OFF_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.video"); - private static final LocTextKey CHAT_ON_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.chat"); - private static final LocTextKey CHAT_OFF_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.chat"); - - private final PageService pageService; - private final GuiServiceInfo guiServiceInfo; - private final String remoteProctoringEndpoint; - private final String remoteProctoringViewServletEndpoint; - public JitsiMeetProctoringView( final PageService pageService, final GuiServiceInfo guiServiceInfo, @Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint, @Value("${sebserver.gui.remote.proctoring.api-servler.endpoint:/remote-view-servlet}") final String remoteProctoringViewServletEndpoint) { - this.pageService = pageService; - this.guiServiceInfo = guiServiceInfo; - this.remoteProctoringEndpoint = remoteProctoringEndpoint; - this.remoteProctoringViewServletEndpoint = remoteProctoringViewServletEndpoint; + super(pageService, guiServiceInfo, remoteProctoringEndpoint, remoteProctoringViewServletEndpoint); } @Override @@ -118,19 +90,18 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { final Composite footer = new Composite(content, SWT.NONE | SWT.NO_SCROLL); footer.setLayout(new RowLayout()); final GridData footerLayout = new GridData(SWT.CENTER, SWT.BOTTOM, true, false); - footerLayout.heightHint = 40; footer.setLayoutData(footerLayout); final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY); - closeAction.setLayoutData(new RowData(150, 30)); + closeAction.setLayoutData(new RowData()); closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringGUIService, proctoringWindowData)); final BroadcastActionState broadcastActionState = new BroadcastActionState(); final Button broadcastAudioAction = widgetFactory.buttonLocalized(footer, BROADCAST_AUDIO_ON_TEXT_KEY); - broadcastAudioAction.setLayoutData(new RowData(150, 30)); + broadcastAudioAction.setLayoutData(new RowData()); broadcastAudioAction.addListener(SWT.Selection, event -> toggleBroadcastAudio( proctoringWindowData.examId, proctoringWindowData.connectionData.roomName, @@ -138,7 +109,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { broadcastAudioAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState); final Button broadcastVideoAction = widgetFactory.buttonLocalized(footer, BROADCAST_VIDEO_ON_TEXT_KEY); - broadcastVideoAction.setLayoutData(new RowData(150, 30)); + broadcastVideoAction.setLayoutData(new RowData()); broadcastVideoAction.addListener(SWT.Selection, event -> toggleBroadcastVideo( proctoringWindowData.examId, proctoringWindowData.connectionData.roomName, @@ -147,7 +118,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { broadcastVideoAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState); final Button chatAction = widgetFactory.buttonLocalized(footer, CHAT_ON_TEXT_KEY); - chatAction.setLayoutData(new RowData(150, 30)); + chatAction.setLayoutData(new RowData()); chatAction.addListener(SWT.Selection, event -> toggleChat( proctoringWindowData.examId, proctoringWindowData.connectionData.roomName, @@ -155,100 +126,4 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { chatAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState); } - private void sendReconfigurationAttributes( - final String examId, - final String roomName, - final BroadcastActionState state) { - - this.pageService.getRestService().getBuilder(SendProctoringReconfigurationAttributes.class) - .withURIVariable(API.PARAM_MODEL_ID, examId) - .withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomName) - .withFormParam( - - API.EXAM_PROCTORING_ATTR_RECEIVE_AUDIO, - state.audio ? Constants.TRUE_STRING : Constants.FALSE_STRING) - .withFormParam( - API.EXAM_PROCTORING_ATTR_RECEIVE_VIDEO, - state.video ? Constants.TRUE_STRING : Constants.FALSE_STRING) - .withFormParam( - API.EXAM_PROCTORING_ATTR_ALLOW_CHAT, - state.chat ? Constants.TRUE_STRING : Constants.FALSE_STRING) - .call() - .onError(error -> log.error("Failed to send broadcast attributes to clients in room: {} cause: {}", - roomName, - error.getMessage())); - - } - - private void toggleBroadcastAudio( - final String examId, - final String roomName, - final Button broadcastAction) { - - final BroadcastActionState state = - (BroadcastActionState) broadcastAction.getData(BroadcastActionState.KEY_NAME); - - this.pageService.getPolyglotPageService().injectI18n( - broadcastAction, - state.audio ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY); - - state.audio = !state.audio; - sendReconfigurationAttributes(examId, roomName, state); - } - - private void toggleBroadcastVideo( - final String examId, - final String roomName, - final Button videoAction, - final Button audioAction) { - - final BroadcastActionState state = - (BroadcastActionState) videoAction.getData(BroadcastActionState.KEY_NAME); - - this.pageService.getPolyglotPageService().injectI18n( - audioAction, - state.video ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY); - this.pageService.getPolyglotPageService().injectI18n( - videoAction, - state.video ? BROADCAST_VIDEO_ON_TEXT_KEY : BROADCAST_VIDEO_OFF_TEXT_KEY); - - state.video = !state.video; - state.audio = state.video; - sendReconfigurationAttributes(examId, roomName, state); - } - - private void toggleChat( - final String examId, - final String roomName, - final Button broadcastAction) { - - final BroadcastActionState state = - (BroadcastActionState) broadcastAction.getData(BroadcastActionState.KEY_NAME); - - this.pageService.getPolyglotPageService().injectI18n( - broadcastAction, - state.chat ? CHAT_ON_TEXT_KEY : CHAT_OFF_TEXT_KEY); - - state.chat = !state.chat; - sendReconfigurationAttributes(examId, roomName, state); - } - - private void closeRoom( - final ProctoringGUIService proctoringGUIService, - final ProctoringWindowData proctoringWindowData) { - - try { - proctoringGUIService.closeRoomWindow(proctoringWindowData.connectionData.roomName); - } catch (final Exception e) { - log.error("Failed to close proctoring window properly: ", e); - } - } - - static final class BroadcastActionState { - public static final String KEY_NAME = "BroadcastActionState"; - boolean audio = false; - boolean video = false; - boolean chat = false; - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ZoomProctoringView.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ZoomProctoringView.java index 6f27c4b4..859c238d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ZoomProctoringView.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ZoomProctoringView.java @@ -23,56 +23,28 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.api.API; -import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gui.GuiServiceInfo; -import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.page.PageContext; import ch.ethz.seb.sebserver.gui.service.page.PageService; -import ch.ethz.seb.sebserver.gui.service.page.RemoteProctoringView; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendProctoringReconfigurationAttributes; import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService; import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; @Component @GuiProfile -public class ZoomProctoringView implements RemoteProctoringView { +public class ZoomProctoringView extends AbstractProctoringView { private static final Logger log = LoggerFactory.getLogger(ZoomProctoringView.class); - private static final LocTextKey CLOSE_WINDOW_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.close"); - private static final LocTextKey BROADCAST_AUDIO_ON_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.audio"); - private static final LocTextKey BROADCAST_AUDIO_OFF_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.audio"); - private static final LocTextKey BROADCAST_VIDEO_ON_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.video"); - private static final LocTextKey BROADCAST_VIDEO_OFF_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.video"); - private static final LocTextKey CHAT_ON_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.chat"); - private static final LocTextKey CHAT_OFF_TEXT_KEY = - new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.chat"); - - private final PageService pageService; - private final GuiServiceInfo guiServiceInfo; - private final String remoteProctoringEndpoint; - private final String remoteProctoringViewServletEndpoint; - public ZoomProctoringView( final PageService pageService, final GuiServiceInfo guiServiceInfo, @Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint, @Value("${sebserver.gui.remote.proctoring.api-servler.endpoint:/remote-view-servlet}") final String remoteProctoringViewServletEndpoint) { - this.pageService = pageService; - this.guiServiceInfo = guiServiceInfo; - this.remoteProctoringEndpoint = remoteProctoringEndpoint; - this.remoteProctoringViewServletEndpoint = remoteProctoringViewServletEndpoint; + super(pageService, guiServiceInfo, remoteProctoringEndpoint, remoteProctoringViewServletEndpoint); } @Override @@ -82,18 +54,20 @@ public class ZoomProctoringView implements RemoteProctoringView { @Override public void compose(final PageContext pageContext) { + final ProctoringWindowData proctoringWindowData = ProctoringGUIService.getCurrentProctoringWindowData(); - final Composite parent = pageContext.getParent(); - final Composite content = new Composite(parent, SWT.NONE | SWT.NO_SCROLL); final GridLayout gridLayout = new GridLayout(); + final ProctoringGUIService proctoringGUIService = this.pageService + .getCurrentUser() + .getProctoringGUIService(); content.setLayout(gridLayout); final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); content.setLayoutData(headerCell); - parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData)); + parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData)); final String url = this.guiServiceInfo .getExternalServerURIBuilder() @@ -116,19 +90,18 @@ public class ZoomProctoringView implements RemoteProctoringView { final Composite footer = new Composite(content, SWT.NONE | SWT.NO_SCROLL); footer.setLayout(new RowLayout()); final GridData footerLayout = new GridData(SWT.CENTER, SWT.BOTTOM, true, false); - footerLayout.heightHint = 40; footer.setLayoutData(footerLayout); final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY); - closeAction.setLayoutData(new RowData(150, 30)); - closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringWindowData)); + closeAction.setLayoutData(new RowData()); + closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringGUIService, proctoringWindowData)); final BroadcastActionState broadcastActionState = new BroadcastActionState(); final Button broadcastAudioAction = widgetFactory.buttonLocalized(footer, BROADCAST_AUDIO_ON_TEXT_KEY); - broadcastAudioAction.setLayoutData(new RowData(150, 30)); + broadcastAudioAction.setLayoutData(new RowData()); broadcastAudioAction.addListener(SWT.Selection, event -> toggleBroadcastAudio( proctoringWindowData.examId, proctoringWindowData.connectionData.roomName, @@ -136,7 +109,7 @@ public class ZoomProctoringView implements RemoteProctoringView { broadcastAudioAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState); final Button broadcastVideoAction = widgetFactory.buttonLocalized(footer, BROADCAST_VIDEO_ON_TEXT_KEY); - broadcastVideoAction.setLayoutData(new RowData(150, 30)); + broadcastVideoAction.setLayoutData(new RowData()); broadcastVideoAction.addListener(SWT.Selection, event -> toggleBroadcastVideo( proctoringWindowData.examId, proctoringWindowData.connectionData.roomName, @@ -145,7 +118,7 @@ public class ZoomProctoringView implements RemoteProctoringView { broadcastVideoAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState); final Button chatAction = widgetFactory.buttonLocalized(footer, CHAT_ON_TEXT_KEY); - chatAction.setLayoutData(new RowData(150, 30)); + chatAction.setLayoutData(new RowData()); chatAction.addListener(SWT.Selection, event -> toggleChat( proctoringWindowData.examId, proctoringWindowData.connectionData.roomName, @@ -154,96 +127,4 @@ public class ZoomProctoringView implements RemoteProctoringView { } - private void sendReconfigurationAttributes( - final String examId, - final String roomName, - final BroadcastActionState state) { - - this.pageService.getRestService().getBuilder(SendProctoringReconfigurationAttributes.class) - .withURIVariable(API.PARAM_MODEL_ID, examId) - .withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomName) - .withFormParam( - - API.EXAM_PROCTORING_ATTR_RECEIVE_AUDIO, - state.audio ? Constants.TRUE_STRING : Constants.FALSE_STRING) - .withFormParam( - API.EXAM_PROCTORING_ATTR_RECEIVE_VIDEO, - state.video ? Constants.TRUE_STRING : Constants.FALSE_STRING) - .withFormParam( - API.EXAM_PROCTORING_ATTR_ALLOW_CHAT, - state.chat ? Constants.TRUE_STRING : Constants.FALSE_STRING) - .call() - .onError(error -> log.error("Failed to send broadcast attributes to clients in room: {} cause: {}", - roomName, - error.getMessage())); - - } - - private void toggleBroadcastAudio( - final String examId, - final String roomName, - final Button broadcastAction) { - - final BroadcastActionState state = - (BroadcastActionState) broadcastAction.getData(BroadcastActionState.KEY_NAME); - - this.pageService.getPolyglotPageService().injectI18n( - broadcastAction, - state.audio ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY); - - state.audio = !state.audio; - sendReconfigurationAttributes(examId, roomName, state); - } - - private void toggleBroadcastVideo( - final String examId, - final String roomName, - final Button videoAction, - final Button audioAction) { - - final BroadcastActionState state = - (BroadcastActionState) videoAction.getData(BroadcastActionState.KEY_NAME); - - this.pageService.getPolyglotPageService().injectI18n( - audioAction, - state.video ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY); - this.pageService.getPolyglotPageService().injectI18n( - videoAction, - state.video ? BROADCAST_VIDEO_ON_TEXT_KEY : BROADCAST_VIDEO_OFF_TEXT_KEY); - - state.video = !state.video; - state.audio = state.video; - sendReconfigurationAttributes(examId, roomName, state); - } - - private void toggleChat( - final String examId, - final String roomName, - final Button broadcastAction) { - - final BroadcastActionState state = - (BroadcastActionState) broadcastAction.getData(BroadcastActionState.KEY_NAME); - - this.pageService.getPolyglotPageService().injectI18n( - broadcastAction, - state.chat ? CHAT_ON_TEXT_KEY : CHAT_OFF_TEXT_KEY); - - state.chat = !state.chat; - sendReconfigurationAttributes(examId, roomName, state); - } - - private void closeRoom(final ProctoringWindowData proctoringWindowData) { - this.pageService - .getCurrentUser() - .getProctoringGUIService() - .closeRoomWindow(proctoringWindowData.windowName); - } - - static final class BroadcastActionState { - public static final String KEY_NAME = "BroadcastActionState"; - boolean audio = false; - boolean video = false; - boolean chat = false; - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java new file mode 100644 index 00000000..e3b18158 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.session.proctoring; + +import java.util.Arrays; +import java.util.Collections; +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.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.TreeItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; +import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; +import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Tuple; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.gui.GuiServiceInfo; +import ch.ethz.seb.sebserver.gui.content.ProctorRoomConnectionsPopup; +import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; +import ch.ethz.seb.sebserver.gui.content.action.ActionPane; +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable; + +@Lazy +@Component +@GuiProfile +public class MonitoringProctoringService { + + private static final Logger log = LoggerFactory.getLogger(MonitoringProctoringService.class); + + private static final LocTextKey EXAM_ROOM_NAME = + new LocTextKey("sebserver.monitoring.exam.proctoring.room.all.name"); + + // @formatter:off + static final String OPEN_ROOM_SCRIPT = + "try {\n" + + "var existingWin = window.open('', '%s', 'height=%s,width=%s,location=no,scrollbars=yes,status=no,menubar=0,toolbar=no,titlebar=no,dialog=no');\n" + + "if(existingWin.location.href === 'about:blank'){\n" + + " existingWin.location.href = '%s%s';\n" + + " existingWin.focus();\n" + + "} else {\n" + + " existingWin.focus();\n" + + "}" + + "}\n" + + "catch(err) {\n" + + " alert(\"Unexpected Javascript Error happened: \" + err);\n"+ + "}"; + // @formatter:on + + private final PageService pageService; + private final GuiServiceInfo guiServiceInfo; + private final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup; + private final String remoteProctoringEndpoint; + + public MonitoringProctoringService( + final PageService pageService, + final GuiServiceInfo guiServiceInfo, + final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup, + @Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint) { + + this.pageService = pageService; + this.guiServiceInfo = guiServiceInfo; + this.proctorRoomConnectionsPopup = proctorRoomConnectionsPopup; + this.remoteProctoringEndpoint = remoteProctoringEndpoint; + } + + public boolean isTownhallRoomActive(final String examModelId) { + return !BooleanUtils.toBoolean(this.pageService + .getRestService() + .getBuilder(IsTownhallRoomAvailable.class) + .withURIVariable(API.PARAM_MODEL_ID, examModelId) + .call() + .getOr(Constants.FALSE_STRING)); + } + + public PageAction toggleTownhallRoom( + final ProctoringGUIService proctoringGUIService, + final PageAction action) { + + if (isTownhallRoomActive(action.getEntityKey().modelId)) { + closeTownhallRoom(proctoringGUIService, action); + this.pageService.firePageEvent( + new ActionActivationEvent( + true, + new Tuple<>( + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)), + action.pageContext()); + return action; + } else { + openTownhallRoom(proctoringGUIService, action); + this.pageService.firePageEvent( + new ActionActivationEvent( + true, + new Tuple<>( + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), + action.pageContext()); + return action; + } + } + + public void initCollectingRoomActions( + final PageContext pageContext, + final PageActionBuilder actionBuilder, + final ProctoringServiceSettings proctoringSettings, + final ProctoringGUIService proctoringGUIService) { + + proctoringGUIService.clearCollectingRoomActionState(); + updateCollectingRoomActions( + pageContext, + actionBuilder, + proctoringSettings, + proctoringGUIService); + } + + public void updateCollectingRoomActions( + final PageContext pageContext, + final PageActionBuilder actionBuilder, + final ProctoringServiceSettings proctoringSettings, + final ProctoringGUIService proctoringGUIService) { + + final EntityKey entityKey = pageContext.getEntityKey(); + final I18nSupport i18nSupport = this.pageService.getI18nSupport(); + + this.pageService + .getRestService() + .getBuilder(GetCollectingRooms.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .onError(error -> log.error("Failed to update proctoring rooms on GUI {}", error.getMessage())) + .getOr(Collections.emptyList()) + .stream() + .forEach(room -> { + if (proctoringGUIService.collectingRoomActionActive(room.name)) { + // update action + final TreeItem treeItem = proctoringGUIService.getCollectingRoomActionItem(room.name); + proctoringGUIService.registerCollectingRoomAction(room, treeItem); + treeItem.setText(i18nSupport.getText(new LocTextKey( + ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM.title.name, + room.subject, + room.roomSize, + proctoringSettings.collectingRoomSize))); + processProctorRoomActionActivation(treeItem, room, pageContext); + } else { + // create new action + final PageAction action = + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM) + .withEntityKey(entityKey) + .withExec(_action -> { + final int actualRoomSize = proctoringGUIService + .getActualCollectingRoomSize(room.name); + if (actualRoomSize <= 0) { + return _action; + } + return showExamProctoringRoom(proctoringSettings, room, _action); + }) + .withNameAttributes( + room.subject, + room.roomSize, + proctoringSettings.collectingRoomSize) + .noEventPropagation() + .create(); + + this.pageService.publishAction( + action, + _treeItem -> proctoringGUIService.registerCollectingRoomAction( + room, + _treeItem, + collectingRoom -> { + final PageContext pc = pageContext.copy() + .clearAttributes() + .withEntityKey(new EntityKey(collectingRoom.name, + EntityType.REMOTE_PROCTORING_ROOM)) + .withParentEntityKey(entityKey); + this.proctorRoomConnectionsPopup.show(pc, collectingRoom.subject); + })); + processProctorRoomActionActivation( + proctoringGUIService.getCollectingRoomActionItem(room.name), + room, pageContext); + } + }); + + updateTownhallButton(proctoringGUIService, pageContext); + } + + public PageAction openExamCollectionProctorScreen( + final PageAction action, + final ClientConnectionData connectionData) { + + final String examId = action.getEntityKey().modelId; + + final ProctoringServiceSettings proctoringSettings = this.pageService.getRestService() + .getBuilder(GetProctoringSettings.class) + .withURIVariable(API.PARAM_MODEL_ID, examId) + .call() + .getOrThrow(); + + final Optional roomOptional = + this.pageService.getRestService().getBuilder(GetCollectingRooms.class) + .withURIVariable(API.PARAM_MODEL_ID, examId) + .call() + .getOrThrow() + .stream() + .filter(room -> room.id.equals(connectionData.clientConnection.remoteProctoringRoomId)) + .findFirst(); + + if (roomOptional.isPresent()) { + final RemoteProctoringRoom room = roomOptional.get(); + final ProctoringRoomConnection proctoringConnectionData = this.pageService + .getRestService() + .getBuilder(GetProctorRoomConnection.class) + .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId)) + .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name) + .withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject)) + .call() + .getOrThrow(); + + ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData); + final String script = String.format( + MonitoringProctoringService.OPEN_ROOM_SCRIPT, + room.name, + 800, + 1200, + this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), + this.remoteProctoringEndpoint); + + RWT.getClient() + .getService(JavaScriptExecutor.class) + .execute(script); + + this.pageService.getCurrentUser() + .getProctoringGUIService() + .registerProctoringWindow(examId, room.name, room.name); + } + + return action; + } + + public PageAction openOneToOneRoom( + final PageAction action, + final ClientConnectionData connectionData, + final ProctoringGUIService proctoringGUIService) { + + final String connectionToken = connectionData.clientConnection.connectionToken; + final String examId = action.getEntityKey().modelId; + + if (!proctoringGUIService.hasWindow(connectionToken)) { + final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService + .openBreakOutRoom( + examId, + connectionToken, + connectionData.clientConnection.userSessionId, + Arrays.asList(connectionToken)) + .onError(error -> log.error( + "Failed to open single proctoring room for connection {} {}", + connectionToken, + error.getMessage())) + .getOr(null); + + ProctoringGUIService.setCurrentProctoringWindowData( + examId, + connectionToken, + proctoringConnectionData); + } + + final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class); + final String script = String.format( + MonitoringProctoringService.OPEN_ROOM_SCRIPT, + connectionToken, + 420, + 640, + this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), + this.remoteProctoringEndpoint); + javaScriptExecutor.execute(script); + return action; + } + + private PageAction openTownhallRoom( + final ProctoringGUIService proctoringGUIService, + final PageAction action) { + + try { + final EntityKey examId = action.getEntityKey(); + + if (proctoringGUIService.getTownhallWindowName(examId.modelId) == null) { + final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService + .openTownhallRoom( + examId.modelId, + this.pageService.getI18nSupport().getText(EXAM_ROOM_NAME)) + .onError(error -> log.error( + "Failed to open all collecting room for exam {} {}", examId.modelId, + error.getMessage())) + .getOrThrow(); + ProctoringGUIService.setCurrentProctoringWindowData( + examId.modelId, + proctoringConnectionData.roomName, + proctoringConnectionData); + } + + final String windowName = proctoringGUIService.getTownhallWindowName(examId.modelId); + final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class); + final String script = String.format( + OPEN_ROOM_SCRIPT, + windowName, + 800, + 1200, + this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), + this.remoteProctoringEndpoint); + javaScriptExecutor.execute(script); + + } catch (final Exception e) { + log.error("Failed to open popup for town-hall room: ", e); + } + return action; + } + + private PageAction closeTownhallRoom( + final ProctoringGUIService proctoringGUIService, + final PageAction action) { + + final String examId = action.getEntityKey().modelId; + try { + + this.pageService + .getCurrentUser() + .getProctoringGUIService() + .closeRoomWindow(proctoringGUIService.getTownhallWindowName(examId)); + + } catch (final Exception e) { + log.error("Failed to close proctoring town-hall room for exam: {}", examId); + } + return action; + } + + private void updateTownhallButton( + final ProctoringGUIService proctoringGUIService, + final PageContext pageContext) { + final EntityKey entityKey = pageContext.getEntityKey(); + + if (isTownhallRoomActive(entityKey.modelId)) { + final boolean townhallRoomFromThisUser = proctoringGUIService + .getTownhallWindowName(entityKey.modelId) != null; + if (townhallRoomFromThisUser) { + this.pageService.firePageEvent( + new ActionActivationEvent( + true, + new Tuple<>( + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), + pageContext); + } else { + this.pageService.firePageEvent( + new ActionActivationEvent( + false, + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), + pageContext); + } + } else { + this.pageService.firePageEvent( + new ActionActivationEvent( + proctoringGUIService.getNumberOfProctoringParticipants() > 0, + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), + pageContext); + } + } + + private void processProctorRoomActionActivation( + final TreeItem treeItem, + final RemoteProctoringRoom room, + final PageContext pageContext) { + + try { + final Display display = pageContext.getRoot().getDisplay(); + final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY); + final Image image = room.roomSize > 0 + ? action.definition.icon.getImage(display) + : action.definition.icon.getGreyedImage(display); + treeItem.setImage(image); + treeItem.setForeground(room.roomSize > 0 ? null : new Color(display, Constants.GREY_DISABLED)); + } catch (final Exception e) { + log.warn("Failed to set Proctor-Room-Activation: ", e.getMessage()); + } + } + + private PageAction showExamProctoringRoom( + final ProctoringServiceSettings proctoringSettings, + final RemoteProctoringRoom room, + final PageAction action) { + + final ProctoringRoomConnection proctoringConnectionData = this.pageService + .getRestService() + .getBuilder(GetProctorRoomConnection.class) + .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId)) + .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name) + .withQueryParam(ProctoringRoomConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject)) + .call() + .getOrThrow(); + + ProctoringGUIService.setCurrentProctoringWindowData( + String.valueOf(proctoringSettings.examId), + proctoringConnectionData); + + final String script = String.format( + OPEN_ROOM_SCRIPT, + room.name, + 800, + 1200, + this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), + this.remoteProctoringEndpoint); + + RWT.getClient() + .getService(JavaScriptExecutor.class) + .execute(script); + + this.pageService.getCurrentUser() + .getProctoringGUIService() + .registerProctoringWindow(String.valueOf(room.examId), room.name, room.name); + + return action; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java index db5c0696..d6bbe9b5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java @@ -11,16 +11,22 @@ package ch.ethz.seb.sebserver.gui.service.session.proctoring; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import org.apache.tomcat.util.buf.StringUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; +import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.util.Pair; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.CloseProctoringRoom; @@ -32,14 +38,82 @@ public class ProctoringGUIService { private static final Logger log = LoggerFactory.getLogger(ProctoringGUIService.class); public static final String SESSION_ATTR_PROCTORING_DATA = "SESSION_ATTR_PROCTORING_DATA"; + private static final String SHOW_CONNECTION_ACTION_APPLIED = "SHOW_CONNECTION_ACTION_APPLIED"; private static final String CLOSE_ROOM_SCRIPT = "var existingWin = window.open('', '%s'); existingWin.close()"; private final RestService restService; final Map openWindows = new HashMap<>(); + final Map> collectingRoomsActionState; public ProctoringGUIService(final RestService restService) { this.restService = restService; + this.collectingRoomsActionState = new HashMap<>(); + } + + public boolean collectingRoomActionActive(final String name) { + return this.collectingRoomsActionState.containsKey(name); + } + + public void registerCollectingRoomAction( + final RemoteProctoringRoom room, + final TreeItem actionItem) { + + this.collectingRoomsActionState.put(room.name, new Pair<>(room, actionItem)); + } + + public void registerCollectingRoomAction( + final RemoteProctoringRoom room, + final TreeItem actionItem, + final Consumer showConnectionsPopup) { + + registerCollectingRoomAction(room, actionItem); + final Tree tree = actionItem.getParent(); + if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) { + tree.addListener(SWT.Selection, event -> { + final TreeItem item = (TreeItem) event.item; + item.getParent().deselectAll(); + if (event.button == 3) { + final RemoteProctoringRoom remoteProctoringRoom = getRemoteProctoringRoom(item); + if (remoteProctoringRoom != null && remoteProctoringRoom.roomSize > 0) { + showConnectionsPopup.accept(remoteProctoringRoom); + //this.proctorRoomConnectionsPopup.show(pc, remoteProctoringRoom.subject); + } + } + }); + tree.setData(SHOW_CONNECTION_ACTION_APPLIED, true); + } + } + + public TreeItem getCollectingRoomActionItem(final String roomName) { + return this.collectingRoomsActionState.get(roomName).b; + } + + private RemoteProctoringRoom getRemoteProctoringRoom(final TreeItem actionItem) { + return this.collectingRoomsActionState.values() + .stream() + .filter(pair -> pair.b.equals(actionItem)) + .findFirst() + .map(pair -> pair.a) + .orElse(null); + } + + public int getActualCollectingRoomSize(final String roomName) { + try { + return this.collectingRoomsActionState.get(roomName).a.roomSize; + } catch (final Exception e) { + log.error("Failed to get actual collecting room size for room: {} cause: ", roomName, e.getMessage()); + return -1; + } + } + + public int getNumberOfProctoringParticipants() { + return this.collectingRoomsActionState.values().stream() + .reduce(0, (acc, room) -> acc + room.a.roomSize, Integer::sum); + } + + public void clearCollectingRoomActionState() { + this.collectingRoomsActionState.clear(); } public void registerProctoringWindow( @@ -50,11 +124,12 @@ public class ProctoringGUIService { this.openWindows.put(windowName, new RoomData(roomName, examId)); } - public boolean isTownhallOpenForUser(final String examId) { + public String getTownhallWindowName(final String examId) { return this.openWindows.values().stream() .filter(room -> room.isTownhall && room.examId.equals(examId)) .findFirst() - .isPresent(); + .map(room -> room.roomName) + .orElse(null); } public static ProctoringWindowData getCurrentProctoringWindowData() { @@ -104,7 +179,6 @@ public class ProctoringGUIService { public Result openTownhallRoom( final String examId, - final String windowName, final String subject) { return this.restService.getBuilder(OpenTownhallRoom.class) @@ -112,7 +186,7 @@ public class ProctoringGUIService { .withFormParam(ProctoringRoomConnection.ATTR_SUBJECT, subject) .call() .map(connection -> { - this.openWindows.put(windowName, new RoomData(connection.roomName, examId, true)); + this.openWindows.put(connection.roomName, new RoomData(connection.roomName, examId, true)); return connection; }); } @@ -134,6 +208,7 @@ public class ProctoringGUIService { } public void clear() { + this.collectingRoomsActionState.clear(); if (!this.openWindows.isEmpty()) { this.openWindows .entrySet() diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolver.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolver.java index ebc9b692..a5cbca8b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolver.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolver.java @@ -123,12 +123,16 @@ public class ZoomWindowScriptResolver implements ProctoringWindowScriptResolver + " console.log(res)\n" + " },\n" + " success: function () {\n" + + " console.log(\"INIT SUCCESS\")\n" + " ZoomMtg.join({\n" + " signature: signature,\n" + " apiKey: API_KEY,\n" + " meetingNumber: config.meetingNumber,\n" + " userName: config.userName,\n" - + " /* passWord: meetConfig.passWord, */\n" + + " passWord: config.passWord,\n" + + " success(res) {\n" + + " console.log(\"JOIN SUCCESS\")\n" + + " },\n" + " error(res) {\n" + " console.warn(\"JOIN ERROR\")\n" + " console.log(res)\n" diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java index 61f6675c..ef3b19e3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java @@ -83,8 +83,8 @@ public class ZoomProctoringService implements ExamProctoringService { "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD = "{\"iss\":\"%s\",\"exp\":%s}"; - private static final String ZOOM_MEETING_ACCESS_TOKEN_PAYLOAD = - "{\"app_key\":\"%s\",\"iat\":%s,\"exp\":%s,\"tpc\":\"%s\",\"pwd\":\"%s\"}"; +// private static final String ZOOM_MEETING_ACCESS_TOKEN_PAYLOAD = +// "{\"app_key\":\"%s\",\"iat\":%s,\"exp\":%s,\"tpc\":\"%s\",\"pwd\":\"%s\"}"; private static final Map SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList( new Tuple<>( @@ -156,7 +156,7 @@ public class ZoomProctoringService implements ExamProctoringService { final ClientCredentials credentials = new ClientCredentials( proctoringSettings.appKey, - proctoringSettings.appSecret); + this.cryptor.decrypt(proctoringSettings.appSecret)); final ResponseEntity result = this.zoomRestTemplate .testServiceConnection( @@ -172,6 +172,19 @@ public class ZoomProctoringService implements ExamProctoringService { // Remove this before finish up the Zoom integration try { + final ProctoringServiceSettings encryptedSettings = new ProctoringServiceSettings( + proctoringSettings.examId, + proctoringSettings.enableProctoring, + proctoringSettings.serverType, + proctoringSettings.serverURL, + proctoringSettings.collectingRoomSize, + proctoringSettings.appKey, + this.cryptor.decrypt(proctoringSettings.appSecret)); + + disposeServiceRoomsForExam( + proctoringSettings.examId, + encryptedSettings) + .getOrThrow(); } catch (final Exception e) { log.error("Failed to dev-cleanup rooms: ", e); @@ -261,7 +274,7 @@ public class ZoomProctoringService implements ExamProctoringService { roomName, subject, jwt, - this.cryptor.decrypt(credentials.accessToken), + credentials.accessToken, credentials.clientId, String.valueOf(additionalZoomRoomData.meeting_id), this.authorizationService.getUserService().getCurrentUser().getUsername()); @@ -307,7 +320,7 @@ public class ZoomProctoringService implements ExamProctoringService { roomName, subject, jwt, - this.cryptor.decrypt(credentials.accessToken), + credentials.accessToken, credentials.clientId, String.valueOf(additionalZoomRoomData.meeting_id), clientConnection.clientConnection.userSessionId); @@ -372,8 +385,13 @@ public class ZoomProctoringService implements ExamProctoringService { roomData.getAdditionalRoomData(), AdditionalZoomRoomData.class); + final ClientCredentials credentials = new ClientCredentials( + proctoringSettings.appKey, + proctoringSettings.appSecret); + this.deleteAdHocMeeting( proctoringSettings, + credentials, roomName, additionalZoomRoomData.user_id) .getOrThrow(); @@ -444,22 +462,19 @@ public class ZoomProctoringService implements ExamProctoringService { return new NewRoom( roomName, subject, - this.cryptor.encrypt(meetingResponse.meetingPwd), + meetingResponse.encryptedMeetingPwd, additionalZoomRoomDataString); }); } private Result deleteAdHocMeeting( final ProctoringServiceSettings proctoringSettings, + final ClientCredentials credentials, final String meetingId, final String userId) { return Result.tryCatch(() -> { - final ClientCredentials credentials = new ClientCredentials( - proctoringSettings.appKey, - this.cryptor.decrypt(proctoringSettings.appSecret)); - this.zoomRestTemplate.deleteMeeting(proctoringSettings.serverURL, credentials, meetingId); this.zoomRestTemplate.deleteUser(proctoringSettings.serverURL, credentials, userId); @@ -472,13 +487,7 @@ public class ZoomProctoringService implements ExamProctoringService { try { - CharSequence decryptedSecret = credentials.secret; - try { - decryptedSecret = this.cryptor.decrypt(credentials.secret); - } catch (final Exception e) { - log.debug("Testing zoom account connection"); - } - + final CharSequence decryptedSecret = this.cryptor.decrypt(credentials.secret); final StringBuilder builder = new StringBuilder(); final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding(); @@ -698,7 +707,10 @@ public class ZoomProctoringService implements ExamProctoringService { return exchange(url, HttpMethod.DELETE, credentials); } catch (final Exception e) { - log.error("Failed to delete Zoom ad-hoc meeting: {}", meetingId, e); + log.error("Failed to delete Zoom ad-hoc meeting: {} cause: {} / {}", + meetingId, + e.getMessage(), + (e.getCause() != null) ? e.getCause().getMessage() : Constants.EMPTY_NOTE); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } @@ -719,7 +731,10 @@ public class ZoomProctoringService implements ExamProctoringService { return exchange(url, HttpMethod.DELETE, credentials); } catch (final Exception e) { - log.error("Failed to delete Zoom ad-hoc user with id: {}", userId, e); + log.error("Failed to delete Zoom ad-hoc user with id: {} cause: {} / {}", + userId, + e.getMessage(), + (e.getCause() != null) ? e.getCause().getMessage() : Constants.EMPTY_NOTE); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } } @@ -741,7 +756,7 @@ public class ZoomProctoringService implements ExamProctoringService { final HttpMethod method, final ClientCredentials credentials) { - return exchange(url, HttpMethod.GET, null, getHeaders(credentials)); + return exchange(url, method, null, getHeaders(credentials)); } private ResponseEntity exchange( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java index 54dbca0c..1d79fb06 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java @@ -148,7 +148,7 @@ public interface ZoomRoomRequestResponse { final String uuid; final String host_id; final CharSequence meetingPwd; - final CharSequence encryptedPwd; + final CharSequence encryptedMeetingPwd; @JsonCreator public MeetingResponse( @@ -161,7 +161,7 @@ public interface ZoomRoomRequestResponse { @JsonProperty("uuid") final String uuid, @JsonProperty("host_id") final String host_id, @JsonProperty("password") final CharSequence meetingPwd, - @JsonProperty("encrypted_password") final CharSequence encryptedPwd) { + @JsonProperty("encrypted_password") final CharSequence encryptedMeetingPwd) { this.id = id; this.join_url = join_url; @@ -172,7 +172,7 @@ public interface ZoomRoomRequestResponse { this.uuid = uuid; this.host_id = host_id; this.meetingPwd = meetingPwd; - this.encryptedPwd = encryptedPwd; + this.encryptedMeetingPwd = encryptedMeetingPwd; } } diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/util/ReplTest.java b/src/test/java/ch/ethz/seb/sebserver/gbl/util/ReplTest.java index a1e91d46..12c5ed74 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gbl/util/ReplTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gbl/util/ReplTest.java @@ -8,29 +8,20 @@ package ch.ethz.seb.sebserver.gbl.util; -import static org.junit.Assert.assertEquals; - -import java.util.UUID; - -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.junit.Ignore; -import org.junit.jupiter.api.Test; - public class ReplTest { - @Test - @Ignore - public void testDateFormatting() { - final String datestring = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss"); - assertEquals("", datestring); - } - - @Test - @Ignore - public void testGenPwd() { - final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9); - assertEquals("", meetingPwd); - } +// @Test +// @Ignore +// public void testDateFormatting() { +// final String datestring = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss"); +// assertEquals("", datestring); +// } +// +// @Test +// @Ignore +// public void testGenPwd() { +// final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9); +// assertEquals("", meetingPwd); +// } }