SEBSERV-204 fixes and improvements

This commit is contained in:
anhefti 2021-07-07 17:20:51 +02:00
parent c7952b32bc
commit 6043d752a6
18 changed files with 244 additions and 138 deletions

View file

@ -36,9 +36,6 @@ public class ProctoringServiceSettings implements Entity {
ONE_TO_ONE, ONE_TO_ONE,
BROADCAST, BROADCAST,
ENABLE_CHAT, ENABLE_CHAT,
WAITING_ROOM,
SEND_REJOIN_COLLECTING_ROOM,
RESET_BROADCAST_ON_LAVE
} }
public static final String ATTR_ENABLE_PROCTORING = "enableProctoring"; public static final String ATTR_ENABLE_PROCTORING = "enableProctoring";

View file

@ -73,6 +73,12 @@ public class RAPConfiguration implements ApplicationConfiguration {
properties.put(WebClient.FAVICON, "fav_icon"); properties.put(WebClient.FAVICON, "fav_icon");
application.addEntryPoint(guiEntrypoint, new RAPSpringEntryPointFactory(), properties); application.addEntryPoint(guiEntrypoint, new RAPSpringEntryPointFactory(), properties);
properties.put(WebClient.PAGE_TITLE, "SEB Server Proctoring");
properties.put(WebClient.BODY_HTML, "<big>Loading Application<big>");
properties.put(WebClient.THEME_ID, DEFAULT_THEME_NAME);
properties.put(WebClient.FAVICON, "fav_icon");
application.addEntryPoint(proctoringEntrypoint, new RAPRemoteProcotringEntryPointFactory(), properties); application.addEntryPoint(proctoringEntrypoint, new RAPRemoteProcotringEntryPointFactory(), properties);
} catch (final RuntimeException re) { } catch (final RuntimeException re) {

View file

@ -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.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; 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.i18n.LocTextKey;
@ -43,6 +44,8 @@ public class MonitoringExamSearchPopup {
new LocTextKey("sebserver.monitoring.search.list.name"); new LocTextKey("sebserver.monitoring.search.list.name");
private static final LocTextKey TABLE_COLUMN_IP_ADDRESS = private static final LocTextKey TABLE_COLUMN_IP_ADDRESS =
new LocTextKey("sebserver.monitoring.search.list.ip"); 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; private final PageService pageService;
@ -50,9 +53,16 @@ public class MonitoringExamSearchPopup {
new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_SESSION_ID); new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_SESSION_ID);
private final TableFilterAttribute ipFilter = private final TableFilterAttribute ipFilter =
new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_IP_STRING); new TableFilterAttribute(CriteriaType.TEXT, ClientConnection.FILTER_ATTR_IP_STRING);
private final TableFilterAttribute statusFilter;
protected MonitoringExamSearchPopup(final PageService pageService) { protected MonitoringExamSearchPopup(final PageService pageService) {
this.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) { public void show(final PageContext pageContext) {
@ -60,6 +70,7 @@ public class MonitoringExamSearchPopup {
pageContext.getParent().getShell(), pageContext.getParent().getShell(),
this.pageService.getWidgetFactory()); this.pageService.getWidgetFactory());
dialog.setLargeDialogWidth(); dialog.setLargeDialogWidth();
dialog.setDialogHeight(380);
dialog.open( dialog.open(
TITLE_TEXT_KEY, TITLE_TEXT_KEY,
pageContext, pageContext,
@ -90,6 +101,12 @@ public class MonitoringExamSearchPopup {
ClientConnection::getClientAddress) ClientConnection::getClientAddress)
.withFilter(this.ipFilter)) .withFilter(this.ipFilter))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_CONNECTION.ATTR_STATUS,
TABLE_COLUMN_STATUS,
ClientConnection::getStatus)
.withFilter(this.statusFilter))
.withDefaultAction(t -> actionBuilder .withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION) .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION)
.withParentEntityKey(examKey) .withParentEntityKey(examKey)

View file

@ -8,21 +8,28 @@
package ch.ethz.seb.sebserver.gui.content; package ch.ethz.seb.sebserver.gui.content;
import org.eclipse.swt.SWT; import java.util.ArrayList;
import org.eclipse.swt.widgets.Composite; import java.util.List;
import org.eclipse.swt.widgets.Label;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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.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.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext; 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;
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.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.service.remote.webservice.api.session.GetCollectingRoomConnections;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
@Lazy @Lazy
@Component @Component
@ -31,6 +38,10 @@ public class ProctorRoomConnectionsPopup {
private static final LocTextKey TITLE_TEXT_KEY = private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.room.connections.title"); 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; private final PageService pageService;
@ -43,29 +54,60 @@ public class ProctorRoomConnectionsPopup {
pageContext.getParent().getShell(), pageContext.getParent().getShell(),
this.pageService.getWidgetFactory()); this.pageService.getWidgetFactory());
dialog.setLargeDialogWidth(); dialog.setLargeDialogWidth();
dialog.setDialogHeight(380);
dialog.open( dialog.open(
new LocTextKey(TITLE_TEXT_KEY.name, roomSubject), new LocTextKey(TITLE_TEXT_KEY.name, roomSubject),
pageContext, pageContext,
this::compose); c -> this.compose(c, dialog));
} }
private void compose(final PageContext pageContext) { private void compose(final PageContext pageContext, final ModalInputDialog<Void> dialog) {
final Composite parent = pageContext.getParent();
final Composite grid = this.pageService.getWidgetFactory().createPopupScrollComposite(parent);
final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey(); final EntityKey parentEntityKey = pageContext.getParentEntityKey();
this.pageService.getRestService() final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final List<ClientConnection> connections = new ArrayList<>(this.pageService.getRestService()
.getBuilder(GetCollectingRoomConnections.class) .getBuilder(GetCollectingRoomConnections.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId) .withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, entityKey.modelId) .withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, entityKey.modelId)
.call() .call()
.getOrThrow() .getOrThrow());
.stream()
.forEach(connection -> { this.pageService.staticListTableBuilder(connections, EntityType.CLIENT_CONNECTION)
final Label label = new Label(grid, SWT.NONE);
label.setText(connection.userSessionId); .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<Void> dialog,
final EntityTable<ClientConnection> 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());
} }
} }

View file

@ -567,6 +567,14 @@ public class ResourceService {
.getText(SEB_CONNECTION_STATUS_KEY_PREFIX + name, name); .getText(SEB_CONNECTION_STATUS_KEY_PREFIX + name, name);
} }
public List<Tuple<String>> 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) { public String localizedExamTypeName(final ExamConfigurationMap examMap) {
if (examMap.examType == null) { if (examMap.examType == null) {
return Constants.EMPTY_NOTE; return Constants.EMPTY_NOTE;

View file

@ -17,6 +17,7 @@ import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; 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); final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
content.setLayoutData(headerCell); 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)); parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData));
final String url = this.guiServiceInfo final String url = this.guiServiceInfo

View file

@ -49,6 +49,7 @@ public class ModalInputDialog<T> extends Dialog {
private int dialogWidth = DEFAULT_DIALOG_WIDTH; private int dialogWidth = DEFAULT_DIALOG_WIDTH;
private int dialogHeight = DEFAULT_DIALOG_HEIGHT; private int dialogHeight = DEFAULT_DIALOG_HEIGHT;
private int buttonWidth = DEFAULT_DIALOG_BUTTON_WIDTH; private int buttonWidth = DEFAULT_DIALOG_BUTTON_WIDTH;
private boolean forceHeight = false;
public ModalInputDialog( public ModalInputDialog(
final Shell parent, final Shell parent,
@ -75,6 +76,7 @@ public class ModalInputDialog<T> extends Dialog {
public ModalInputDialog<T> setDialogHeight(final int dialogHeight) { public ModalInputDialog<T> setDialogHeight(final int dialogHeight) {
this.dialogHeight = dialogHeight; this.dialogHeight = dialogHeight;
this.forceHeight = true;
return this; return this;
} }
@ -215,6 +217,9 @@ public class ModalInputDialog<T> extends Dialog {
} }
private int calcDialogHeight(final Composite main) { private int calcDialogHeight(final Composite main) {
if (this.forceHeight) {
return this.dialogHeight;
}
final int actualHeight = main.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; final int actualHeight = main.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
final int displayHeight = main.getDisplay().getClientArea().height; final int displayHeight = main.getDisplay().getClientArea().height;
final int availableHeight = (displayHeight < actualHeight + 100) final int availableHeight = (displayHeight < actualHeight + 100)

View file

@ -17,6 +17,7 @@ import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; 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); final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
content.setLayoutData(headerCell); 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)); parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData));
final String url = this.guiServiceInfo final String url = this.guiServiceInfo

View file

@ -10,7 +10,6 @@ package ch.ethz.seb.sebserver.gui.service.session.proctoring;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Optional;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.rap.rwt.RWT; 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.event.ActionActivationEvent;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; 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.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.GetCollectingRooms;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; 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.api.session.IsTownhallRoomAvailable;
@ -81,6 +79,7 @@ public class MonitoringProctoringService {
static final String OPEN_ROOM_SCRIPT = static final String OPEN_ROOM_SCRIPT =
"try {\n" + "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" + "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" + "if(existingWin.location.href === 'about:blank'){\n" +
" existingWin.location.href = '%s%s';\n" + " existingWin.location.href = '%s%s';\n" +
" existingWin.focus();\n" + " existingWin.focus();\n" +
@ -208,7 +207,7 @@ public class MonitoringProctoringService {
if (actualRoomSize <= 0) { if (actualRoomSize <= 0) {
return _action; return _action;
} }
return showExamProctoringRoom(proctoringSettings, room, _action); return openExamProctoringRoom(proctoringSettings, room, _action);
}) })
.withNameAttributes( .withNameAttributes(
room.subject, room.subject,
@ -239,30 +238,11 @@ public class MonitoringProctoringService {
updateTownhallButton(proctoringGUIService, pageContext); updateTownhallButton(proctoringGUIService, pageContext);
} }
public PageAction openExamCollectionProctorScreen( private PageAction openExamProctoringRoom(
final PageAction action, final ProctoringServiceSettings proctoringSettings,
final ClientConnectionData connectionData) { final RemoteProctoringRoom room,
final PageAction action) {
try {
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<RemoteProctoringRoom> 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 final ProctoringRoomConnection proctoringConnectionData = this.pageService
.getRestService() .getRestService()
.getBuilder(GetProctorRoomConnection.class) .getBuilder(GetProctorRoomConnection.class)
@ -272,12 +252,16 @@ public class MonitoringProctoringService {
.call() .call()
.getOrThrow(); .getOrThrow();
ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData); ProctoringGUIService.setCurrentProctoringWindowData(
String.valueOf(proctoringSettings.examId),
proctoringConnectionData);
final String script = String.format( final String script = String.format(
MonitoringProctoringService.OPEN_ROOM_SCRIPT, OPEN_ROOM_SCRIPT,
room.name, room.name,
800, 800,
1200, 1200,
room.name,
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint); this.remoteProctoringEndpoint);
@ -285,14 +269,17 @@ public class MonitoringProctoringService {
.getService(JavaScriptExecutor.class) .getService(JavaScriptExecutor.class)
.execute(script); .execute(script);
this.pageService.getCurrentUser() final boolean newWindow = this.pageService.getCurrentUser()
.getProctoringGUIService() .getProctoringGUIService()
.registerProctoringWindow(examId, room.name, room.name); .registerProctoringWindow(String.valueOf(room.examId), room.name, room.name);
}
} catch (final Exception e) { if (newWindow) {
log.error("Failed to open popup for collecting room: ", e); this.pageService.getRestService()
action.pageContext().notifyError(CLOSE_COLLECTING_ERROR, e); .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; return action;
@ -333,6 +320,7 @@ public class MonitoringProctoringService {
connectionToken, connectionToken,
420, 420,
640, 640,
connectionData.clientConnection.userSessionId,
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint); this.remoteProctoringEndpoint);
javaScriptExecutor.execute(script); javaScriptExecutor.execute(script);
@ -374,6 +362,7 @@ public class MonitoringProctoringService {
windowName, windowName,
800, 800,
1200, 1200,
"Town-Hall",
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
this.remoteProctoringEndpoint); this.remoteProctoringEndpoint);
javaScriptExecutor.execute(script); 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;
}
} }

View file

@ -117,12 +117,14 @@ public class ProctoringGUIService {
this.collectingRoomsActionState.clear(); this.collectingRoomsActionState.clear();
} }
public void registerProctoringWindow( public boolean registerProctoringWindow(
final String examId, final String examId,
final String windowName, final String windowName,
final String roomName) { 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) { public String getTownhallWindowName(final String examId) {

View file

@ -121,9 +121,15 @@ public interface RemoteProctoringRoomDAO {
/** Get currently active break-out rooms for given connectionToken /** 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 * @param connectionTokens The connection token of the client connection
* @return Result refer to active break-out rooms or to an error when happened */ * @return Result refer to active break-out rooms or to an error when happened */
Result<Collection<RemoteProctoringRoom>> getBreakoutRooms(String connectionToken); Result<Collection<RemoteProctoringRoom>> 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<Collection<String>> getConnectionsInBreakoutRooms(Long examId);
} }

View file

@ -322,6 +322,24 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }
@Override
@Transactional(readOnly = true)
public Result<Collection<String>> 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) { private RemoteProctoringRoom toDomainModel(final RemoteProctoringRoomRecord record) {
final String breakOutConnections = record.getBreakOutConnections(); final String breakOutConnections = record.getBreakOutConnections();
final Collection<String> connections = StringUtils.isNotBlank(breakOutConnections) final Collection<String> connections = StringUtils.isNotBlank(breakOutConnections)

View file

@ -33,13 +33,20 @@ public interface ExamProctoringRoomService {
* @return Result refer to the resulting collection of ClientConnection or to an error when happened */ * @return Result refer to the resulting collection of ClientConnection or to an error when happened */
Result<Collection<ClientConnection>> getRoomConnections(Long roomId); Result<Collection<ClientConnection>> 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 examId The exam identifier of the room
* @param roomName The room name * @param roomName The room name
* @return Result refer to the resulting collection of ClientConnection or to an error when happened */ * @return Result refer to the resulting collection of ClientConnection or to an error when happened */
Result<Collection<ClientConnection>> getCollectingRoomConnections(Long examId, String roomName); Result<Collection<ClientConnection>> 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<Collection<ClientConnection>> getActiveCollectingRoomConnections(Long examId, String roomName);
/** This is internally used to update client connections that are flagged for updating /** This is internally used to update client connections that are flagged for updating
* the proctoring room assignment. * the proctoring room assignment.
* This attaches or detaches client connections from or to proctoring rooms of an exam in one batch. * This attaches or detaches client connections from or to proctoring rooms of an exam in one batch.

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -18,6 +19,7 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; 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.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; 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;
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.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; 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 ExamAdminService examAdminService;
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final SEBClientInstructionService sebInstructionService; private final SEBClientInstructionService sebInstructionService;
private final boolean sendBroadcastReset;
public ExamProctoringRoomServiceImpl( public ExamProctoringRoomServiceImpl(
final RemoteProctoringRoomDAO remoteProctoringRoomDAO, final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
final ClientConnectionDAO clientConnectionDAO, final ClientConnectionDAO clientConnectionDAO,
final ExamAdminService examAdminService, final ExamAdminService examAdminService,
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final SEBClientInstructionService sebInstructionService) { final SEBClientInstructionService sebInstructionService,
@Value("${sebserver.webservice.proctoring.resetBroadcastOnLeav:true}") final boolean sendBroadcastReset) {
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.clientConnectionDAO = clientConnectionDAO; this.clientConnectionDAO = clientConnectionDAO;
this.examAdminService = examAdminService; this.examAdminService = examAdminService;
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.sebInstructionService = sebInstructionService; this.sebInstructionService = sebInstructionService;
this.sendBroadcastReset = sendBroadcastReset;
} }
@Override @Override
@ -81,8 +85,34 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
} }
@Override @Override
public Result<Collection<ClientConnection>> getCollectingRoomConnections(final Long examId, final String roomName) { public Result<Collection<ClientConnection>> getCollectingRoomConnections(
return this.clientConnectionDAO.getCollectingRoomConnections(examId, roomName); final Long examId,
final String roomName) {
return this.clientConnectionDAO
.getCollectingRoomConnections(examId, roomName);
}
@Override
public Result<Collection<ClientConnection>> getActiveCollectingRoomConnections(
final Long examId,
final String roomName) {
final Collection<String> 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 @Override
@ -357,8 +387,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.notifyBreakOutRoomOpened(proctoringSettings, room) .notifyBreakOutRoomOpened(proctoringSettings, room)
.getOrThrow(); .getOrThrow();
} else { } else {
final Collection<ClientConnection> clientConnections = this.clientConnectionDAO final Collection<ClientConnection> clientConnections = this
.getCollectingRoomConnections(examId, roomName) .getActiveCollectingRoomConnections(examId, roomName)
.getOrThrow(); .getOrThrow();
examProctoringService examProctoringService
@ -379,7 +409,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getOrThrow(); .getOrThrow();
// Send default settings to clients if fearture is enabled // Send default settings to clients if fearture is enabled
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) { if (this.sendBroadcastReset) {
this.sendReconfigurationInstructions( this.sendReconfigurationInstructions(
examId, examId,
connectionTokens, connectionTokens,
@ -410,14 +440,14 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
final ExamProctoringService examProctoringService) { final ExamProctoringService examProctoringService) {
// get all connections of the room // get all connections of the room
final List<String> connectionTokens = this.getCollectingRoomConnections(examId, roomName) final List<String> connectionTokens = this.getActiveCollectingRoomConnections(examId, roomName)
.getOrThrow() .getOrThrow()
.stream() .stream()
.map(cc -> cc.connectionToken) .map(cc -> cc.connectionToken)
.collect(Collectors.toList()); .collect(Collectors.toList());
// Send default settings to clients if feature is enabled // Send default settings to clients if feature is enabled
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) { if (this.sendBroadcastReset) {
this.sendReconfigurationInstructions( this.sendReconfigurationInstructions(
examId, examId,
connectionTokens, connectionTokens,
@ -460,7 +490,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
final RemoteProctoringRoom remoteProctoringRoom) { final RemoteProctoringRoom remoteProctoringRoom) {
// Send default settings to clients if feature is enabled // Send default settings to clients if feature is enabled
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.RESET_BROADCAST_ON_LAVE)) { if (this.sendBroadcastReset) {
this.sendReconfigurationInstructions( this.sendReconfigurationInstructions(
examId, examId,
remoteProctoringRoom.breakOutConnections, remoteProctoringRoom.breakOutConnections,
@ -517,7 +547,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
} else if (!room.breakOutConnections.isEmpty()) { } else if (!room.breakOutConnections.isEmpty()) {
connectionTokens.addAll(room.breakOutConnections); connectionTokens.addAll(room.breakOutConnections);
} else { } else {
connectionTokens.addAll(this.getCollectingRoomConnections(examId, roomName) connectionTokens.addAll(this.getActiveCollectingRoomConnections(examId, roomName)
.getOrThrow() .getOrThrow()
.stream() .stream()
.map(cc -> cc.connectionToken) .map(cc -> cc.connectionToken)

View file

@ -29,6 +29,7 @@ import org.joda.time.DateTimeZone;
import org.joda.time.Interval; import org.joda.time.Interval;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; 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.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; 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;
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.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
@ -126,6 +126,8 @@ public class ZoomProctoringService implements ExamProctoringService {
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
private final SEBClientInstructionService sebInstructionService; private final SEBClientInstructionService sebInstructionService;
private final boolean enableWaitingRoom;
private final boolean sendRejoinForCollectingRoom;
public ZoomProctoringService( public ZoomProctoringService(
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
@ -135,7 +137,9 @@ public class ZoomProctoringService implements ExamProctoringService {
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
final RemoteProctoringRoomDAO remoteProctoringRoomDAO, final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
final AuthorizationService authorizationService, 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.examSessionService = examSessionService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
@ -146,6 +150,8 @@ public class ZoomProctoringService implements ExamProctoringService {
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.sebInstructionService = sebInstructionService; this.sebInstructionService = sebInstructionService;
this.enableWaitingRoom = enableWaitingRoom;
this.sendRejoinForCollectingRoom = sendRejoinForCollectingRoom;
} }
@Override @Override
@ -444,13 +450,13 @@ public class ZoomProctoringService implements ExamProctoringService {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
if (!proctoringSettings.enabledFeatures.contains(ProctoringFeature.SEND_REJOIN_COLLECTING_ROOM)) { if (!this.sendRejoinForCollectingRoom) {
// do nothing if the rejoin feature is not enabled // does nothing if the rejoin feature is not enabled
return; return;
} }
if (this.remoteProctoringRoomDAO.isTownhallRoomActive(proctoringSettings.examId)) { 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 // the meeting once the town-hall has been closed
return; return;
} }
@ -522,7 +528,7 @@ public class ZoomProctoringService implements ExamProctoringService {
subject, subject,
duration, duration,
meetingPwd, meetingPwd,
proctoringSettings.enabledFeatures.contains(ProctoringFeature.WAITING_ROOM)); this.enableWaitingRoom);
final MeetingResponse meetingResponse = this.jsonMapper.readValue( final MeetingResponse meetingResponse = this.jsonMapper.readValue(
createMeeting.getBody(), createMeeting.getBody(),

View file

@ -138,8 +138,7 @@ public interface ZoomRoomRequestResponse {
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static class Settings { static class Settings {
@JsonProperty final boolean host_video = false; @JsonProperty final boolean host_video = false;
@JsonProperty final boolean participant_video = false; @JsonProperty final boolean mute_upon_entry = false;
@JsonProperty final boolean mute_upon_entry = true;
@JsonProperty final boolean join_before_host; @JsonProperty final boolean join_before_host;
@JsonProperty final int jbh_time = 0; @JsonProperty final int jbh_time = 0;
@JsonProperty final boolean use_pmi = false; @JsonProperty final boolean use_pmi = false;

View file

@ -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.moodle.api.token.request.paths=/login/token.php
sebserver.webservice.lms.address.alias= sebserver.webservice.lms.address.alias=
sebserver.webservice.proctoring.resetBroadcastOnLeav=true
sebserver.webservice.proctoring.zoom.enableWaitingRoom=false
sebserver.webservice.proctoring.zoom.sendRejoinForCollectingRoom=true

View file

@ -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.showTownhall=Show Townhall
sebserver.monitoring.exam.action.proctoring.closeTownhall=Close 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.close=Close Window
sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast
sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End 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.empty=No Client Connections available
sebserver.monitoring.search.list.name=Session or User Name sebserver.monitoring.search.list.name=Session or User Name
sebserver.monitoring.search.list.ip=IP Address 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 sebserver.monitoring.exam.connection.emptySelection=At first please select a Connection from the list