SEBSERV-148 implementation und fixes

This commit is contained in:
anhefti 2021-03-31 08:21:03 +02:00
parent 6ff8b703c9
commit 29bda22a40
12 changed files with 789 additions and 767 deletions

View file

@ -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<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
.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;

View file

@ -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<String, Pair<RemoteProctoringRoom, TreeItem>> 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<String, Pair<RemoteProctoringRoom, TreeItem>> 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<String, Pair<RemoteProctoringRoom, TreeItem>> 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<String, Pair<RemoteProctoringRoom, TreeItem>> 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<PageAction, PageAction> 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";
}
}

View file

@ -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. */

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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<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
.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;
}
}

View file

@ -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<String, RoomData> openWindows = new HashMap<>();
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> 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<RemoteProctoringRoom> 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<ProctoringRoomConnection> 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()

View file

@ -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"

View file

@ -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<String, String> 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<String> 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<Void> 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<String> exchange(

View file

@ -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;
}
}

View file

@ -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);
// }
}