diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java index 74fe7982..4d65b4fc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java @@ -36,9 +36,6 @@ public class ProctoringServiceSettings implements Entity { ONE_TO_ONE, BROADCAST, ENABLE_CHAT, - WAITING_ROOM, - SEND_REJOIN_COLLECTING_ROOM, - RESET_BROADCAST_ON_LAVE } public static final String ATTR_ENABLE_PROCTORING = "enableProctoring"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java b/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java index 17be4e81..31096dd2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java @@ -73,6 +73,12 @@ public class RAPConfiguration implements ApplicationConfiguration { properties.put(WebClient.FAVICON, "fav_icon"); application.addEntryPoint(guiEntrypoint, new RAPSpringEntryPointFactory(), properties); + + properties.put(WebClient.PAGE_TITLE, "SEB Server Proctoring"); + properties.put(WebClient.BODY_HTML, "Loading Application"); + properties.put(WebClient.THEME_ID, DEFAULT_THEME_NAME); + properties.put(WebClient.FAVICON, "fav_icon"); + application.addEntryPoint(proctoringEntrypoint, new RAPRemoteProcotringEntryPointFactory(), properties); } catch (final RuntimeException re) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringExamSearchPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringExamSearchPopup.java index 70df7769..2f6a6de2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringExamSearchPopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringExamSearchPopup.java @@ -15,6 +15,7 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; @@ -43,6 +44,8 @@ public class MonitoringExamSearchPopup { new LocTextKey("sebserver.monitoring.search.list.name"); private static final LocTextKey TABLE_COLUMN_IP_ADDRESS = new LocTextKey("sebserver.monitoring.search.list.ip"); + private static final LocTextKey TABLE_COLUMN_STATUS = + new LocTextKey("sebserver.monitoring.search.list.status"); private final PageService pageService; @@ -50,9 +53,16 @@ public class MonitoringExamSearchPopup { new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_SESSION_ID); private final TableFilterAttribute ipFilter = new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_IP_STRING); + private final TableFilterAttribute statusFilter; protected MonitoringExamSearchPopup(final PageService pageService) { this.pageService = pageService; + + this.statusFilter = new TableFilterAttribute( + CriteriaType.SINGLE_SELECTION, + ClientConnection.FILTER_ATTR_STATUS, + ConnectionStatus.ACTIVE.name(), + pageService.getResourceService()::localizedClientConnectionStatusResources); } public void show(final PageContext pageContext) { @@ -60,6 +70,7 @@ public class MonitoringExamSearchPopup { pageContext.getParent().getShell(), this.pageService.getWidgetFactory()); dialog.setLargeDialogWidth(); + dialog.setDialogHeight(380); dialog.open( TITLE_TEXT_KEY, pageContext, @@ -90,6 +101,12 @@ public class MonitoringExamSearchPopup { ClientConnection::getClientAddress) .withFilter(this.ipFilter)) + .withColumn(new ColumnDefinition<>( + Domain.CLIENT_CONNECTION.ATTR_STATUS, + TABLE_COLUMN_STATUS, + ClientConnection::getStatus) + .withFilter(this.statusFilter)) + .withDefaultAction(t -> actionBuilder .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION) .withParentEntityKey(examKey) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ProctorRoomConnectionsPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ProctorRoomConnectionsPopup.java index 4891aca9..87a367eb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ProctorRoomConnectionsPopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ProctorRoomConnectionsPopup.java @@ -8,21 +8,28 @@ package ch.ethz.seb.sebserver.gui.content; -import org.eclipse.swt.SWT; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Label; +import java.util.ArrayList; +import java.util.List; + import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; 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.impl.ModalInputDialog; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRoomConnections; +import ch.ethz.seb.sebserver.gui.table.ColumnDefinition; +import ch.ethz.seb.sebserver.gui.table.EntityTable; @Lazy @Component @@ -31,6 +38,10 @@ public class ProctorRoomConnectionsPopup { private static final LocTextKey TITLE_TEXT_KEY = new LocTextKey("sebserver.monitoring.exam.proctoring.room.connections.title"); + private static final LocTextKey EMPTY_LIST_TEXT_KEY = + new LocTextKey("sebserver.monitoring.search.list.empty"); + private static final LocTextKey TABLE_COLUMN_NAME = + new LocTextKey("sebserver.monitoring.search.list.name"); private final PageService pageService; @@ -43,29 +54,60 @@ public class ProctorRoomConnectionsPopup { pageContext.getParent().getShell(), this.pageService.getWidgetFactory()); dialog.setLargeDialogWidth(); + dialog.setDialogHeight(380); dialog.open( new LocTextKey(TITLE_TEXT_KEY.name, roomSubject), pageContext, - this::compose); + c -> this.compose(c, dialog)); } - private void compose(final PageContext pageContext) { - final Composite parent = pageContext.getParent(); - final Composite grid = this.pageService.getWidgetFactory().createPopupScrollComposite(parent); + private void compose(final PageContext pageContext, final ModalInputDialog dialog) { final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey parentEntityKey = pageContext.getParentEntityKey(); - this.pageService.getRestService() + final PageActionBuilder actionBuilder = this.pageService + .pageActionBuilder(pageContext.clearEntityKeys()); + + final List connections = new ArrayList<>(this.pageService.getRestService() .getBuilder(GetCollectingRoomConnections.class) .withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId) .withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, entityKey.modelId) .call() - .getOrThrow() - .stream() - .forEach(connection -> { - final Label label = new Label(grid, SWT.NONE); - label.setText(connection.userSessionId); - }); + .getOrThrow()); + + this.pageService.staticListTableBuilder(connections, EntityType.CLIENT_CONNECTION) + + .withEmptyMessage(EMPTY_LIST_TEXT_KEY) + .withPaging(10) + + .withColumn(new ColumnDefinition<>( + Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID, + TABLE_COLUMN_NAME, + ClientConnection::getUserSessionId)) + + .withDefaultAction(t -> actionBuilder + .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION) + .withParentEntityKey(parentEntityKey) + .withExec(action -> showClientConnection(action, dialog, t)) + .create()) + + .compose(pageContext); + } + + private PageAction showClientConnection( + final PageAction pageAction, + final ModalInputDialog dialog, + final EntityTable table) { + + final ClientConnection singleSelectedROWData = table.getSingleSelectedROWData(); + dialog.close(); + return pageAction + .withEntityKey(new EntityKey( + singleSelectedROWData.id, + EntityType.CLIENT_CONNECTION)) + .withAttribute( + Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN, + singleSelectedROWData.getConnectionToken()); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java index d6d76114..3fe55d5e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java @@ -567,6 +567,14 @@ public class ResourceService { .getText(SEB_CONNECTION_STATUS_KEY_PREFIX + name, name); } + public List> localizedClientConnectionStatusResources() { + return Arrays.stream(ConnectionStatus.values()) + .map(type -> new Tuple<>(type.name(), + this.i18nSupport.getText(SEB_CONNECTION_STATUS_KEY_PREFIX + type.name()))) + .sorted(RESOURCE_COMPARATOR) + .collect(Collectors.toList()); + } + public String localizedExamTypeName(final ExamConfigurationMap examMap) { if (examMap.examType == null) { return Constants.EMPTY_NOTE; 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 f94c230f..8ffae1b5 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 @@ -17,6 +17,7 @@ import org.eclipse.swt.layout.RowData; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -78,6 +79,11 @@ public class JitsiMeetProctoringView extends AbstractProctoringView { final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); content.setLayoutData(headerCell); + final Label title = this.pageService + .getWidgetFactory() + .label(content, proctoringWindowData.connectionData.subject); + title.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, true, true)); + parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData)); final String url = this.guiServiceInfo diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java index e5cc439e..3aba4629 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ModalInputDialog.java @@ -49,6 +49,7 @@ public class ModalInputDialog extends Dialog { private int dialogWidth = DEFAULT_DIALOG_WIDTH; private int dialogHeight = DEFAULT_DIALOG_HEIGHT; private int buttonWidth = DEFAULT_DIALOG_BUTTON_WIDTH; + private boolean forceHeight = false; public ModalInputDialog( final Shell parent, @@ -75,6 +76,7 @@ public class ModalInputDialog extends Dialog { public ModalInputDialog setDialogHeight(final int dialogHeight) { this.dialogHeight = dialogHeight; + this.forceHeight = true; return this; } @@ -215,6 +217,9 @@ public class ModalInputDialog extends Dialog { } private int calcDialogHeight(final Composite main) { + if (this.forceHeight) { + return this.dialogHeight; + } final int actualHeight = main.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; final int displayHeight = main.getDisplay().getClientArea().height; final int availableHeight = (displayHeight < actualHeight + 100) 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 044ecdb8..311bb3b6 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 @@ -17,6 +17,7 @@ import org.eclipse.swt.layout.RowData; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -78,6 +79,11 @@ public class ZoomProctoringView extends AbstractProctoringView { final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); content.setLayoutData(headerCell); + final Label title = this.pageService + .getWidgetFactory() + .label(content, proctoringWindowData.connectionData.subject); + title.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, true, false)); + parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData)); final String url = this.guiServiceInfo 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 index 296bd102..986581b1 100644 --- 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 @@ -10,7 +10,6 @@ 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; @@ -48,7 +47,6 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.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; @@ -81,6 +79,7 @@ public class MonitoringProctoringService { 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" + + "existingWin.document.title = '%s';\n" + "if(existingWin.location.href === 'about:blank'){\n" + " existingWin.location.href = '%s%s';\n" + " existingWin.focus();\n" + @@ -208,7 +207,7 @@ public class MonitoringProctoringService { if (actualRoomSize <= 0) { return _action; } - return showExamProctoringRoom(proctoringSettings, room, _action); + return openExamProctoringRoom(proctoringSettings, room, _action); }) .withNameAttributes( room.subject, @@ -239,60 +238,48 @@ public class MonitoringProctoringService { updateTownhallButton(proctoringGUIService, pageContext); } - public PageAction openExamCollectionProctorScreen( - final PageAction action, - final ClientConnectionData connectionData) { + private PageAction openExamProctoringRoom( + final ProctoringServiceSettings proctoringSettings, + final RemoteProctoringRoom room, + final PageAction action) { - try { - final String examId = action.getEntityKey().modelId; + 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(); - final ProctoringServiceSettings proctoringSettings = this.pageService.getRestService() - .getBuilder(GetProctoringSettings.class) - .withURIVariable(API.PARAM_MODEL_ID, examId) + ProctoringGUIService.setCurrentProctoringWindowData( + String.valueOf(proctoringSettings.examId), + proctoringConnectionData); + + final String script = String.format( + OPEN_ROOM_SCRIPT, + room.name, + 800, + 1200, + room.name, + this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), + this.remoteProctoringEndpoint); + + RWT.getClient() + .getService(JavaScriptExecutor.class) + .execute(script); + + final boolean newWindow = this.pageService.getCurrentUser() + .getProctoringGUIService() + .registerProctoringWindow(String.valueOf(room.examId), room.name, room.name); + + if (newWindow) { + this.pageService.getRestService() + .getBuilder(NotifyProctoringRoomOpened.class) + .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId)) + .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name) .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); - } - - } catch (final Exception e) { - log.error("Failed to open popup for collecting room: ", e); - action.pageContext().notifyError(CLOSE_COLLECTING_ERROR, e); + .onError(error -> log.error("Failed to notify proctoring room opened: ", error)); } return action; @@ -333,6 +320,7 @@ public class MonitoringProctoringService { connectionToken, 420, 640, + connectionData.clientConnection.userSessionId, this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.remoteProctoringEndpoint); javaScriptExecutor.execute(script); @@ -374,6 +362,7 @@ public class MonitoringProctoringService { windowName, 800, 1200, + "Town-Hall", this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.remoteProctoringEndpoint); javaScriptExecutor.execute(script); @@ -460,47 +449,4 @@ public class MonitoringProctoringService { } } - 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); - - this.pageService.getRestService().getBuilder(NotifyProctoringRoomOpened.class) - .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId)) - .withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name) - .call() - .onError(error -> log.error("Failed to notify proctoring room opened: ", error)); - - 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 9dd5af67..318fec50 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 @@ -117,12 +117,14 @@ public class ProctoringGUIService { this.collectingRoomsActionState.clear(); } - public void registerProctoringWindow( + public boolean registerProctoringWindow( final String examId, final String windowName, final String roomName) { - this.openWindows.put(windowName, new RoomData(roomName, examId)); + return this.openWindows.putIfAbsent( + windowName, + new RoomData(roomName, examId)) == null; } public String getTownhallWindowName(final String examId) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java index a3f28fc3..27c86813 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java @@ -121,9 +121,15 @@ public interface RemoteProctoringRoomDAO { /** Get currently active break-out rooms for given connectionToken * - * @param examId The exam identifier of the connection * @param connectionTokens The connection token of the client connection * @return Result refer to active break-out rooms or to an error when happened */ Result> getBreakoutRooms(String connectionToken); + /** Get a list of client connection tokens of connections that currently are in + * break-out rooms, including the town-hall room + * + * @param examId The exam identifier of the connection + * @return Result refer to active break-out rooms or to an error when happened */ + Result> getConnectionsInBreakoutRooms(Long examId); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java index 0f49fc08..b6f4858d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java @@ -322,6 +322,24 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { .collect(Collectors.toList())); } + @Override + @Transactional(readOnly = true) + public Result> getConnectionsInBreakoutRooms(final Long examId) { + return Result.tryCatch(() -> this.remoteProctoringRoomRecordMapper + .selectByExample() + .where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId)) + .and(RemoteProctoringRoomRecordDynamicSqlSupport.breakOutConnections, isNotNull()) + .build() + .execute() + .stream() + .flatMap(room -> Arrays.asList( + StringUtils.split( + room.getBreakOutConnections(), + Constants.LIST_SEPARATOR_CHAR)) + .stream()) + .collect(Collectors.toList())); + } + private RemoteProctoringRoom toDomainModel(final RemoteProctoringRoomRecord record) { final String breakOutConnections = record.getBreakOutConnections(); final Collection connections = StringUtils.isNotBlank(breakOutConnections) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java index 0165176a..a6091b49 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java @@ -33,13 +33,20 @@ public interface ExamProctoringRoomService { * @return Result refer to the resulting collection of ClientConnection or to an error when happened */ Result> getRoomConnections(Long roomId); - /** Get a collection of all ClientConnection that are currently connected to a specified collecting room. + /** Get a collection of all ClientConnection that are registered to a specified collecting room. * * @param examId The exam identifier of the room * @param roomName The room name * @return Result refer to the resulting collection of ClientConnection or to an error when happened */ Result> getCollectingRoomConnections(Long examId, String roomName); + /** Get a collection of all ClientConnection that are currently connected to a specified collecting room. + * + * @param examId The exam identifier of the room + * @param roomName The room name + * @return Result refer to the resulting collection of ClientConnection or to an error when happened */ + Result> getActiveCollectingRoomConnections(Long examId, String roomName); + /** This is internally used to update client connections that are flagged for updating * the proctoring room assignment. * This attaches or detaches client connections from or to proctoring rooms of an exam in one batch. diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java index e8157a49..6112c865 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -18,6 +19,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; 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.Service; @@ -27,7 +29,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; -import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; @@ -55,19 +56,22 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService private final ExamAdminService examAdminService; private final ExamSessionService examSessionService; private final SEBClientInstructionService sebInstructionService; + private final boolean sendBroadcastReset; public ExamProctoringRoomServiceImpl( final RemoteProctoringRoomDAO remoteProctoringRoomDAO, final ClientConnectionDAO clientConnectionDAO, final ExamAdminService examAdminService, final ExamSessionService examSessionService, - final SEBClientInstructionService sebInstructionService) { + final SEBClientInstructionService sebInstructionService, + @Value("${sebserver.webservice.proctoring.resetBroadcastOnLeav:true}") final boolean sendBroadcastReset) { this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.clientConnectionDAO = clientConnectionDAO; this.examAdminService = examAdminService; this.examSessionService = examSessionService; this.sebInstructionService = sebInstructionService; + this.sendBroadcastReset = sendBroadcastReset; } @Override @@ -81,8 +85,34 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService } @Override - public Result> getCollectingRoomConnections(final Long examId, final String roomName) { - return this.clientConnectionDAO.getCollectingRoomConnections(examId, roomName); + public Result> getCollectingRoomConnections( + final Long examId, + final String roomName) { + + return this.clientConnectionDAO + .getCollectingRoomConnections(examId, roomName); + } + + @Override + public Result> getActiveCollectingRoomConnections( + final Long examId, + final String roomName) { + + final Collection currentlyInBreakoutRooms = this.remoteProctoringRoomDAO + .getConnectionsInBreakoutRooms(examId) + .getOrElse(() -> Collections.emptyList()); + + if (currentlyInBreakoutRooms.isEmpty()) { + return this.clientConnectionDAO + .getCollectingRoomConnections(examId, roomName); + } else { + return this.clientConnectionDAO + .getCollectingRoomConnections(examId, roomName) + .map(connections -> connections + .stream() + .filter(cc -> !currentlyInBreakoutRooms.contains(cc.connectionToken)) + .collect(Collectors.toList())); + } } @Override @@ -357,8 +387,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService .notifyBreakOutRoomOpened(proctoringSettings, room) .getOrThrow(); } else { - final Collection clientConnections = this.clientConnectionDAO - .getCollectingRoomConnections(examId, roomName) + final Collection clientConnections = this + .getActiveCollectingRoomConnections(examId, roomName) .getOrThrow(); examProctoringService @@ -379,7 +409,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService .getOrThrow(); // Send default settings to clients if fearture is enabled - if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) { + if (this.sendBroadcastReset) { this.sendReconfigurationInstructions( examId, connectionTokens, @@ -410,14 +440,14 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService final ExamProctoringService examProctoringService) { // get all connections of the room - final List connectionTokens = this.getCollectingRoomConnections(examId, roomName) + final List connectionTokens = this.getActiveCollectingRoomConnections(examId, roomName) .getOrThrow() .stream() .map(cc -> cc.connectionToken) .collect(Collectors.toList()); // Send default settings to clients if feature is enabled - if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) { + if (this.sendBroadcastReset) { this.sendReconfigurationInstructions( examId, connectionTokens, @@ -460,7 +490,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService final RemoteProctoringRoom remoteProctoringRoom) { // Send default settings to clients if feature is enabled - if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) { + if (this.sendBroadcastReset) { this.sendReconfigurationInstructions( examId, remoteProctoringRoom.breakOutConnections, @@ -517,7 +547,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService } else if (!room.breakOutConnections.isEmpty()) { connectionTokens.addAll(room.breakOutConnections); } else { - connectionTokens.addAll(this.getCollectingRoomConnections(examId, roomName) + connectionTokens.addAll(this.getActiveCollectingRoomConnections(examId, roomName) .getOrThrow() .stream() .map(cc -> cc.connectionToken) 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 80fd682b..20e38042 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 @@ -29,6 +29,7 @@ import org.joda.time.DateTimeZone; import org.joda.time.Interval; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -57,7 +58,6 @@ import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; 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.exam.ProctoringServiceSettings.ProctoringFeature; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; @@ -126,6 +126,8 @@ public class ZoomProctoringService implements ExamProctoringService { private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; private final AuthorizationService authorizationService; private final SEBClientInstructionService sebInstructionService; + private final boolean enableWaitingRoom; + private final boolean sendRejoinForCollectingRoom; public ZoomProctoringService( final ExamSessionService examSessionService, @@ -135,7 +137,9 @@ public class ZoomProctoringService implements ExamProctoringService { final JSONMapper jsonMapper, final RemoteProctoringRoomDAO remoteProctoringRoomDAO, final AuthorizationService authorizationService, - final SEBClientInstructionService sebInstructionService) { + final SEBClientInstructionService sebInstructionService, + @Value("${sebserver.webservice.proctoring.enableWaitingRoom:false}") final boolean enableWaitingRoom, + @Value("${sebserver.webservice.proctoring.sendRejoinForCollectingRoom:true}") final boolean sendRejoinForCollectingRoom) { this.examSessionService = examSessionService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; @@ -146,6 +150,8 @@ public class ZoomProctoringService implements ExamProctoringService { this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.authorizationService = authorizationService; this.sebInstructionService = sebInstructionService; + this.enableWaitingRoom = enableWaitingRoom; + this.sendRejoinForCollectingRoom = sendRejoinForCollectingRoom; } @Override @@ -444,13 +450,13 @@ public class ZoomProctoringService implements ExamProctoringService { return Result.tryCatch(() -> { - if (!proctoringSettings.enabledFeatures.contains(ProctoringFeature.SEND_REJOIN_COLLECTING_ROOM)) { - // do nothing if the rejoin feature is not enabled + if (!this.sendRejoinForCollectingRoom) { + // does nothing if the rejoin feature is not enabled return; } if (this.remoteProctoringRoomDAO.isTownhallRoomActive(proctoringSettings.examId)) { - // do nothing if the town-hall of this exam is open. The clients will automatically join + // does nothing if the town-hall of this exam is open. The clients will automatically join // the meeting once the town-hall has been closed return; } @@ -522,7 +528,7 @@ public class ZoomProctoringService implements ExamProctoringService { subject, duration, meetingPwd, - proctoringSettings.enabledFeatures.contains(ProctoringFeature.WAITING_ROOM)); + this.enableWaitingRoom); final MeetingResponse meetingResponse = this.jsonMapper.readValue( createMeeting.getBody(), 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 5ea1492e..d822da64 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 @@ -138,8 +138,7 @@ public interface ZoomRoomRequestResponse { @JsonIgnoreProperties(ignoreUnknown = true) static class Settings { @JsonProperty final boolean host_video = false; - @JsonProperty final boolean participant_video = false; - @JsonProperty final boolean mute_upon_entry = true; + @JsonProperty final boolean mute_upon_entry = false; @JsonProperty final boolean join_before_host; @JsonProperty final int jbh_time = 0; @JsonProperty final boolean use_pmi = false; diff --git a/src/main/resources/config/application-ws.properties b/src/main/resources/config/application-ws.properties index 511ce231..2f579802 100644 --- a/src/main/resources/config/application-ws.properties +++ b/src/main/resources/config/application-ws.properties @@ -66,3 +66,7 @@ sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.moodle.api.token.request.paths=/login/token.php sebserver.webservice.lms.address.alias= +sebserver.webservice.proctoring.resetBroadcastOnLeav=true +sebserver.webservice.proctoring.zoom.enableWaitingRoom=false +sebserver.webservice.proctoring.zoom.sendRejoinForCollectingRoom=true + diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index ecda90f4..f4131b0b 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1633,7 +1633,7 @@ sebserver.monitoring.exam.action.proctoring.openTownhall=Open Townhall sebserver.monitoring.exam.action.proctoring.showTownhall=Show Townhall sebserver.monitoring.exam.action.proctoring.closeTownhall=Close Townhall -sebserver.monitoring.exam.proctoring.room.all.name=Exam Room +sebserver.monitoring.exam.proctoring.room.all.name=Townhall Room sebserver.monitoring.exam.proctoring.action.close=Close Window sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End Audio Broadcast @@ -1679,6 +1679,7 @@ sebserver.monitoring.search.action=Search sebserver.monitoring.search.list.empty=No Client Connections available sebserver.monitoring.search.list.name=Session or User Name sebserver.monitoring.search.list.ip=IP Address +sebserver.monitoring.search.list.status=Status sebserver.monitoring.exam.connection.emptySelection=At first please select a Connection from the list