SEBSERV-435 gui and monitoring implementation

This commit is contained in:
anhefti 2023-10-05 16:27:11 +02:00
parent 1cbc97ef8f
commit a04c111b59
21 changed files with 524 additions and 246 deletions

View file

@ -217,6 +217,7 @@ public final class API {
public static final String EXAM_PROCTORING_ENDPOINT = EXAM_MONITORING_ENDPOINT + "/proctoring"; public static final String EXAM_PROCTORING_ENDPOINT = EXAM_MONITORING_ENDPOINT + "/proctoring";
public static final String EXAM_PROCTORING_COLLECTING_ROOMS_SEGMENT = "/collecting-rooms"; public static final String EXAM_PROCTORING_COLLECTING_ROOMS_SEGMENT = "/collecting-rooms";
public static final String EXAM_SCREEN_PROCTORING_GROUPS_SEGMENT = "/screenproctoring-groups";
public static final String EXAM_PROCTORING_OPEN_BREAK_OUT_ROOM_SEGMENT = "/open"; public static final String EXAM_PROCTORING_OPEN_BREAK_OUT_ROOM_SEGMENT = "/open";
public static final String EXAM_PROCTORING_CLOSE_ROOM_SEGMENT = "/close"; public static final String EXAM_PROCTORING_CLOSE_ROOM_SEGMENT = "/close";
public static final String EXAM_PROCTORING_NOTIFY_OPEN_ROOM_SEGMENT = "/notify-open-room"; public static final String EXAM_PROCTORING_NOTIFY_OPEN_ROOM_SEGMENT = "/notify-open-room";

View file

@ -15,12 +15,14 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class MonitoringFullPageData { public class MonitoringFullPageData {
public static final String ATTR_CONNECTIONS_DATA = "monitoringConnectionData"; public static final String ATTR_CONNECTIONS_DATA = "monitoringConnectionData";
public static final String ATTR_PROCTORING_DATA = "proctoringData"; public static final String ATTR_PROCTORING_DATA = "proctoringData";
public static final String ATTR_SCREEN_PROCTORING_DATA = "screenProctoringData";
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID)
public final Long examId; public final Long examId;
@ -28,15 +30,19 @@ public class MonitoringFullPageData {
public final MonitoringSEBConnectionData monitoringConnectionData; public final MonitoringSEBConnectionData monitoringConnectionData;
@JsonProperty(ATTR_PROCTORING_DATA) @JsonProperty(ATTR_PROCTORING_DATA)
public final Collection<RemoteProctoringRoom> proctoringData; public final Collection<RemoteProctoringRoom> proctoringData;
@JsonProperty(ATTR_SCREEN_PROCTORING_DATA)
final Collection<ScreenProctoringGroup> screenProctoringData;
public MonitoringFullPageData( public MonitoringFullPageData(
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId, @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId,
@JsonProperty(ATTR_CONNECTIONS_DATA) final MonitoringSEBConnectionData monitoringConnectionData, @JsonProperty(ATTR_CONNECTIONS_DATA) final MonitoringSEBConnectionData monitoringConnectionData,
@JsonProperty(ATTR_PROCTORING_DATA) final Collection<RemoteProctoringRoom> proctoringData) { @JsonProperty(ATTR_PROCTORING_DATA) final Collection<RemoteProctoringRoom> proctoringData,
@JsonProperty(ATTR_SCREEN_PROCTORING_DATA) final Collection<ScreenProctoringGroup> screenProctoringData) {
this.examId = examId; this.examId = examId;
this.monitoringConnectionData = monitoringConnectionData; this.monitoringConnectionData = monitoringConnectionData;
this.proctoringData = proctoringData; this.proctoringData = proctoringData;
this.screenProctoringData = screenProctoringData;
} }
public Long getExamId() { public Long getExamId() {
@ -51,6 +57,10 @@ public class MonitoringFullPageData {
return this.proctoringData; return this.proctoringData;
} }
public Collection<ScreenProctoringGroup> getScreenProctoringData() {
return this.screenProctoringData;
}
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
@ -79,12 +89,14 @@ public class MonitoringFullPageData {
@Override @Override
public String toString() { public String toString() {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
builder.append("OverallMonitroingData [examId="); builder.append("MonitoringFullPageData [examId=");
builder.append(this.examId); builder.append(this.examId);
builder.append(", monitoringConnectionData="); builder.append(", monitoringConnectionData=");
builder.append(this.monitoringConnectionData); builder.append(this.monitoringConnectionData);
builder.append(", proctoringData="); builder.append(", proctoringData=");
builder.append(this.proctoringData); builder.append(this.proctoringData);
builder.append(", screenProctoringData=");
builder.append(this.screenProctoringData);
builder.append("]"); builder.append("]");
return builder.toString(); return builder.toString();
} }

View file

@ -45,6 +45,9 @@ public class RAPSpringConfig {
@Value("${sebserver.gui.remote.proctoring.api-servler.endpoint:/remote-view-servlet}") @Value("${sebserver.gui.remote.proctoring.api-servler.endpoint:/remote-view-servlet}")
private String remoteProctoringViewServletEndpoint; private String remoteProctoringViewServletEndpoint;
@Value("${sebserver.gui.screen.proctoring.api-servler.endpoint:/screen-proctoring}")
private String screenProctoringViewServletEndpoint;
@Bean @Bean
public StaticApplicationPropertyResolver staticApplicationPropertyResolver() { public StaticApplicationPropertyResolver staticApplicationPropertyResolver() {
return new StaticApplicationPropertyResolver(); return new StaticApplicationPropertyResolver();
@ -83,6 +86,17 @@ public class RAPSpringConfig {
this.remoteProctoringEndpoint + this.remoteProctoringViewServletEndpoint + "/*"); this.remoteProctoringEndpoint + this.remoteProctoringViewServletEndpoint + "/*");
} }
@Bean
public ServletRegistrationBean<ScreenProctoringServlet> servletScreenProctoringRegistrationBean(
final ApplicationContext applicationContext) {
final ScreenProctoringServlet proctoringServlet = applicationContext
.getBean(ScreenProctoringServlet.class);
return new ServletRegistrationBean<>(
proctoringServlet,
this.remoteProctoringEndpoint + this.screenProctoringViewServletEndpoint + "/*");
}
@Bean @Bean
public MessageSource messageSource() { public MessageSource messageSource() {
final ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = final ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource =

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2023 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;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
@Component
@GuiProfile
public class ScreenProctoringServlet extends HttpServlet {
private static final long serialVersionUID = 4147410676185956971L;
@Override
protected void doGet(
final HttpServletRequest req,
final HttpServletResponse resp) throws ServletException, IOException {
final HttpSession httpSession = req.getSession();
final ServletContext servletContext = httpSession.getServletContext();
final WebApplicationContext webApplicationContext = WebApplicationContextUtils
.getRequiredWebApplicationContext(servletContext);
final UserInfo user = isAuthenticated(httpSession, webApplicationContext);
// TODO https://stackoverflow.com/questions/46582/response-redirect-with-post-instead-of-get
final String hello = "Hello";
// StringBuilder sb = new StringBuilder();
// sb.Append("<html>");
// sb.AppendFormat(@"<body onload='document.forms[""form""].submit()'>");
// sb.AppendFormat("<form name='form' action='{0}' method='post'>",postbackUrl);
// sb.AppendFormat("<input type='hidden' name='id' value='{0}'>", id);
// // Other params go here
// sb.Append("</form>");
// sb.Append("</body>");
// sb.Append("</html>");
resp.getOutputStream().println(hello);
}
private UserInfo isAuthenticated(
final HttpSession httpSession,
final WebApplicationContext webApplicationContext) {
final AuthorizationContextHolder authorizationContextHolder = webApplicationContext
.getBean(AuthorizationContextHolder.class);
final SEBServerAuthorizationContext authorizationContext = authorizationContextHolder
.getAuthorizationContext(httpSession);
if (!authorizationContext.isValid() || !authorizationContext.isLoggedIn()) {
throw new RuntimeException("No authentication found");
}
return authorizationContext.getLoggedInUser().getOrThrow();
}
}

View file

@ -48,6 +48,7 @@ public enum ActionCategory {
STATE_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.statefilter"), 40), STATE_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.statefilter"), 40),
GROUP_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.groupfilter"), 50), GROUP_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.groupfilter"), 50),
PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.proctoring"), 60), PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.proctoring"), 60),
SCREEN_PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.screenproctoring"), 65),
FINISHED_EXAM_LIST(new LocTextKey("sebserver.finished.exam.list.actions"), 1); FINISHED_EXAM_LIST(new LocTextKey("sebserver.finished.exam.list.actions"), 1);

View file

@ -1035,11 +1035,11 @@ public enum ActionDefinition {
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.CLIENT_EVENT_LIST), ActionCategory.CLIENT_EVENT_LIST),
MONITOR_EXAM_NEW_PROCTOR_ROOM( // MONITOR_EXAM_NEW_PROCTOR_ROOM(
new LocTextKey("sebserver.monitoring.exam.action.newroom"), // new LocTextKey("sebserver.monitoring.exam.action.newroom"),
ImageIcon.VISIBILITY, // ImageIcon.VISIBILITY,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, // PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.PROCTORING), // ActionCategory.PROCTORING),
MONITOR_EXAM_VIEW_PROCTOR_ROOM( MONITOR_EXAM_VIEW_PROCTOR_ROOM(
new LocTextKey("sebserver.monitoring.exam.action.viewroom"), new LocTextKey("sebserver.monitoring.exam.action.viewroom"),
ImageIcon.PROCTOR_ROOM, ImageIcon.PROCTOR_ROOM,
@ -1056,6 +1056,12 @@ public enum ActionDefinition {
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.PROCTORING), ActionCategory.PROCTORING),
MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP(
new LocTextKey("sebserver.monitoring.exam.action.viewgroup"),
ImageIcon.SCREEN_PROC_ON,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.SCREEN_PROCTORING),
FINISHED_EXAM_VIEW_LIST( FINISHED_EXAM_VIEW_LIST(
new LocTextKey("sebserver.finished.action.list"), new LocTextKey("sebserver.finished.action.list"),
PageStateDefinitionImpl.FINISHED_EXAM_LIST), PageStateDefinitionImpl.FINISHED_EXAM_LIST),

View file

@ -76,8 +76,6 @@ public class ScreenProctoringSettingsPopup {
new LocTextKey("sebserver.exam.sps.form.accountId"); new LocTextKey("sebserver.exam.sps.form.accountId");
private final static LocTextKey FORM_ACCOUNT_SECRET_SPS = private final static LocTextKey FORM_ACCOUNT_SECRET_SPS =
new LocTextKey("sebserver.exam.sps.form.accountSecret"); new LocTextKey("sebserver.exam.sps.form.accountSecret");
private final static LocTextKey FORM_COLLECT_STRATEGY =
new LocTextKey("sebserver.exam.sps.form.collect.strategy");
private final static LocTextKey SAVE_TEXT_KEY = private final static LocTextKey SAVE_TEXT_KEY =
new LocTextKey("sebserver.exam.sps.form.saveSettings"); new LocTextKey("sebserver.exam.sps.form.saveSettings");

View file

@ -18,12 +18,15 @@ import java.util.function.BooleanSupplier;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.text.StringEscapeUtils; import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.TreeItem; import org.eclipse.swt.widgets.TreeItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -38,7 +41,10 @@ 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.Indicator;
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.ProctoringFeature;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
@ -60,8 +66,11 @@ import ch.ethz.seb.sebserver.gui.service.push.ServerPushService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.clientgroup.GetClientGroups; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.clientgroup.GetClientGroups;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.indicator.GetIndicators; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.indicator.GetIndicators;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetScreenProctoringGroups;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; 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.ClientConnectionTable;
import ch.ethz.seb.sebserver.gui.service.session.FullPageMonitoringGUIUpdate; import ch.ethz.seb.sebserver.gui.service.session.FullPageMonitoringGUIUpdate;
@ -76,6 +85,8 @@ import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService
@GuiProfile @GuiProfile
public class MonitoringRunningExam implements TemplateComposer { public class MonitoringRunningExam implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(MonitoringRunningExam.class);
private static final LocTextKey EMPTY_SELECTION_TEXT_KEY = private static final LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.emptySelection"); new LocTextKey("sebserver.monitoring.exam.connection.emptySelection");
private static final LocTextKey EMPTY_ACTIVE_SELECTION_TEXT_KEY = private static final LocTextKey EMPTY_ACTIVE_SELECTION_TEXT_KEY =
@ -194,8 +205,7 @@ public class MonitoringRunningExam implements TemplateComposer {
ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION, ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION,
ActionDefinition.MONITOR_EXAM_QUIT_SELECTED, ActionDefinition.MONITOR_EXAM_QUIT_SELECTED,
ActionDefinition.MONITOR_EXAM_LOCK_SELECTED, ActionDefinition.MONITOR_EXAM_LOCK_SELECTED,
ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION, ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION));
ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM));
actionBuilder actionBuilder
@ -275,14 +285,19 @@ public class MonitoringRunningExam implements TemplateComposer {
.call() .call()
.getOr(null); .getOr(null);
if (proctoringSettings != null && proctoringSettings.enableProctoring) { final ScreenProctoringSettings screenProctoringSettings = this.restService
guiUpdates.add(createProctoringActions( .getBuilder(GetScreenProctoringSettings.class)
proctoringSettings, .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
currentUser.getProctoringGUIService(), .call()
pageContext, .getOr(null);
content,
actionBuilder)); guiUpdates.add(createProctoringActions(
} proctoringSettings,
screenProctoringSettings,
currentUser.getProctoringGUIService(),
pageContext,
content));
} }
// finally start the page update (server push) // finally start the page update (server push)
@ -304,12 +319,20 @@ public class MonitoringRunningExam implements TemplateComposer {
private FullPageMonitoringGUIUpdate createProctoringActions( private FullPageMonitoringGUIUpdate createProctoringActions(
final ProctoringServiceSettings proctoringSettings, final ProctoringServiceSettings proctoringSettings,
final ScreenProctoringSettings screenProctoringSettings,
final ProctoringGUIService proctoringGUIService, final ProctoringGUIService proctoringGUIService,
final PageContext pageContext, final PageContext pageContext,
final Composite parent, final Composite parent) {
final PageActionBuilder actionBuilder) {
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) { final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final boolean proctoringEnabled = proctoringSettings != null &&
BooleanUtils.toBoolean(proctoringSettings.enableProctoring);
final boolean screenProctoringEnabled = screenProctoringSettings != null &&
BooleanUtils.toBoolean(proctoringSettings.enableProctoring);
if (proctoringEnabled && proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) {
final EntityKey entityKey = pageContext.getEntityKey(); final EntityKey entityKey = pageContext.getEntityKey();
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)
.withEntityKey(entityKey) .withEntityKey(entityKey)
@ -338,18 +361,44 @@ public class MonitoringRunningExam implements TemplateComposer {
} }
} }
this.monitoringProctoringService.initCollectingRoomActions( proctoringGUIService.clearCollectingRoomActionState();
pageContext, final EntityKey entityKey = pageContext.getEntityKey();
actionBuilder, final Collection<RemoteProctoringRoom> collectingRooms = (proctoringEnabled)
proctoringSettings, ? this.pageService
proctoringGUIService); .getRestService()
.getBuilder(GetCollectingRooms.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> log.error("Failed to get collecting room data:", error))
.getOr(Collections.emptyList())
: Collections.emptyList();
return monitoringStatus -> this.monitoringProctoringService.updateCollectingRoomActions( final Collection<ScreenProctoringGroup> screenProctoringGroups = (screenProctoringEnabled)
monitoringStatus.proctoringData(), ? this.pageService
.getRestService()
.getBuilder(GetScreenProctoringGroups.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> log.error("Failed to get collecting room data:", error))
.getOr(Collections.emptyList())
: Collections.emptyList();
this.monitoringProctoringService.updateCollectingRoomActions(
collectingRooms,
screenProctoringGroups,
pageContext, pageContext,
actionBuilder,
proctoringSettings, proctoringSettings,
proctoringGUIService); proctoringGUIService,
screenProctoringSettings);
return monitoringStatus -> this.monitoringProctoringService
.updateCollectingRoomActions(
monitoringStatus.proctoringData(),
monitoringStatus.screenProctoringData(),
pageContext,
proctoringSettings,
proctoringGUIService,
screenProctoringSettings);
} }
private FullPageMonitoringGUIUpdate createFilterActions( private FullPageMonitoringGUIUpdate createFilterActions(
@ -430,8 +479,7 @@ public class MonitoringRunningExam implements TemplateComposer {
.noEventPropagation() .noEventPropagation()
.withSwitchAction( .withSwitchAction(
actionBuilder.newAction(hideActionDef) actionBuilder.newAction(hideActionDef)
.withExec( .withExec(hideStateViewAction(filter, clientTable, status))
hideStateViewAction(filter, clientTable, status))
.noEventPropagation() .noEventPropagation()
.withNameAttributes(numOfConnections) .withNameAttributes(numOfConnections)
.create()) .create())
@ -443,8 +491,7 @@ public class MonitoringRunningExam implements TemplateComposer {
.noEventPropagation() .noEventPropagation()
.withSwitchAction( .withSwitchAction(
actionBuilder.newAction(showActionDef) actionBuilder.newAction(showActionDef)
.withExec( .withExec(showStateViewAction(filter, clientTable, status))
showStateViewAction(filter, clientTable, status))
.noEventPropagation() .noEventPropagation()
.withNameAttributes(numOfConnections) .withNameAttributes(numOfConnections)
.create()) .create())

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
import java.util.Collection;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetScreenProctoringGroups extends RestCall<Collection<ScreenProctoringGroup>> {
public GetScreenProctoringGroups() {
super(new TypeKey<>(
CallType.GET_LIST,
EntityType.SCREEN_PROCTORING_GROUP,
new TypeReference<Collection<ScreenProctoringGroup>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_SCREEN_PROCTORING_GROUPS_SEGMENT);
}
}

View file

@ -16,6 +16,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringData; import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringData;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData;
@ -95,4 +96,13 @@ public interface MonitoringFilter {
} }
} }
default Collection<ScreenProctoringGroup> screenProctoringData() {
final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData();
if (monitoringFullPageData != null) {
return monitoringFullPageData.getScreenProctoringData();
} else {
return null;
}
}
} }

View file

@ -8,9 +8,9 @@
package ch.ethz.seb.sebserver.gui.service.session.proctoring; package ch.ethz.seb.sebserver.gui.service.session.proctoring;
import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
@ -18,6 +18,7 @@ import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
import org.eclipse.rap.rwt.client.service.UrlLauncher;
import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Display;
@ -27,7 +28,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
@ -39,8 +43,10 @@ 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.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.ProctoringFeature;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
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.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Tuple; import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
@ -55,7 +61,6 @@ 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.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.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;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.NotifyProctoringRoomOpened; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.NotifyProctoringRoomOpened;
@ -93,6 +98,9 @@ public class MonitoringProctoringService {
private final Resource openRoomScriptRes; private final Resource openRoomScriptRes;
private final String remoteProctoringEndpoint; private final String remoteProctoringEndpoint;
@Value("${sebserver.gui.screen.proctoring.api-servler.endpoint:/screen-proctoring}")
private String screenProctoringViewServletEndpoint;
public MonitoringProctoringService( public MonitoringProctoringService(
final PageService pageService, final PageService pageService,
final GuiServiceInfo guiServiceInfo, final GuiServiceInfo guiServiceInfo,
@ -152,87 +160,124 @@ public class MonitoringProctoringService {
return action; return action;
} }
public void initCollectingRoomActions(
final PageContext pageContext,
final PageActionBuilder actionBuilder,
final ProctoringServiceSettings proctoringSettings,
final ProctoringGUIService proctoringGUIService) {
proctoringGUIService.clearCollectingRoomActionState();
final EntityKey entityKey = pageContext.getEntityKey();
final Collection<RemoteProctoringRoom> collectingRooms = this.pageService
.getRestService()
.getBuilder(GetCollectingRooms.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> log.error("Failed to get collecting room data:", error))
.getOr(Collections.emptyList());
updateCollectingRoomActions(
collectingRooms,
pageContext,
actionBuilder,
proctoringSettings,
proctoringGUIService);
}
public void updateCollectingRoomActions( public void updateCollectingRoomActions(
final Collection<RemoteProctoringRoom> collectingRooms, final Collection<RemoteProctoringRoom> collectingRooms,
final Collection<ScreenProctoringGroup> screenProctoringGroups,
final PageContext pageContext, final PageContext pageContext,
final PageActionBuilder actionBuilder,
final ProctoringServiceSettings proctoringSettings, final ProctoringServiceSettings proctoringSettings,
final ProctoringGUIService proctoringGUIService) { final ProctoringGUIService proctoringGUIService,
final ScreenProctoringSettings screenProctoringSettings) {
final EntityKey entityKey = pageContext.getEntityKey();
final I18nSupport i18nSupport = this.pageService.getI18nSupport();
collectingRooms collectingRooms
.stream() .stream()
.forEach(room -> { .forEach(room -> updateProctoringAction(
if (proctoringGUIService.collectingRoomActionActive(room.name)) { pageContext,
// update action proctoringSettings,
final TreeItem treeItem = proctoringGUIService.getCollectingRoomActionItem(room.name); proctoringGUIService,
proctoringGUIService.registerCollectingRoomAction(room, treeItem); room));
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 -> openExamProctoringRoom(
proctoringGUIService,
proctoringSettings,
room,
_action))
.withNameAttributes(
room.subject,
room.roomSize,
proctoringSettings.collectingRoomSize)
.noEventPropagation()
.create();
this.pageService.publishAction(
action,
_treeItem -> proctoringGUIService.registerCollectingRoomAction(
room,
_treeItem,
collectingRoom -> showCollectingRoomPopup(pageContext, entityKey,
collectingRoom)));
processProctorRoomActionActivation(
proctoringGUIService.getCollectingRoomActionItem(room.name),
room, pageContext);
}
});
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) { if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) {
updateTownhallButton(proctoringGUIService, pageContext); updateTownhallButton(proctoringGUIService, pageContext);
} }
if (screenProctoringGroups != null) {
screenProctoringGroups
.stream()
.forEach(group -> updateScreenProctoringAction(
pageContext,
screenProctoringSettings,
proctoringGUIService,
group));
}
}
private void updateScreenProctoringAction(
final PageContext pageContext,
final ScreenProctoringSettings settings,
final ProctoringGUIService proctoringGUIService,
final ScreenProctoringGroup group) {
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final EntityKey entityKey = pageContext.getEntityKey();
final I18nSupport i18nSupport = this.pageService.getI18nSupport();
final TreeItem screeProcotringGroupAction = proctoringGUIService.getScreeProcotringGroupAction(group);
if (screeProcotringGroupAction != null) {
// update action
screeProcotringGroupAction.setText(i18nSupport.getText(new LocTextKey(
ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP.title.name,
group.name,
group.size)));
} else {
// create action
this.pageService.publishAction(
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP)
.withEntityKey(entityKey)
.withExec(_action -> openScreenProctoringTab(
settings,
group,
_action))
.withNameAttributes(
group.name,
group.size)
.noEventPropagation()
.create(),
_treeItem -> proctoringGUIService.registerScreeProcotringGroupAction(group, _treeItem));
}
}
private void updateProctoringAction(
final PageContext pageContext,
final ProctoringServiceSettings proctoringSettings,
final ProctoringGUIService proctoringGUIService,
final RemoteProctoringRoom room) {
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final EntityKey entityKey = pageContext.getEntityKey();
final I18nSupport i18nSupport = this.pageService.getI18nSupport();
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 -> openExamProctoringRoom(
proctoringGUIService,
proctoringSettings,
room,
_action))
.withNameAttributes(
room.subject,
room.roomSize,
proctoringSettings.collectingRoomSize)
.noEventPropagation()
.create();
this.pageService.publishAction(
action,
_treeItem -> proctoringGUIService.registerCollectingRoomAction(
room,
_treeItem,
collectingRoom -> showCollectingRoomPopup(pageContext, entityKey,
collectingRoom)));
processProctorRoomActionActivation(
proctoringGUIService.getCollectingRoomActionItem(room.name),
room, pageContext);
}
} }
private void showCollectingRoomPopup( private void showCollectingRoomPopup(
@ -263,6 +308,28 @@ public class MonitoringProctoringService {
this.proctorRoomConnectionsPopup.show(pc, collectingRoom.subject); this.proctorRoomConnectionsPopup.show(pc, collectingRoom.subject);
} }
private PageAction openScreenProctoringTab(
final ScreenProctoringSettings settings,
final ScreenProctoringGroup group,
final PageAction _action) {
final String serviceRedirect = settings.spsServiceURL + "/guilogin";
final ResponseEntity<Void> redirect = new RestTemplate().exchange(
serviceRedirect,
HttpMethod.GET,
null,
Void.class);
final URI redirectLocation = redirect.getHeaders().getLocation();
final UrlLauncher launcher = RWT.getClient().getService(UrlLauncher.class);
final String url = this.remoteProctoringEndpoint
+ this.screenProctoringViewServletEndpoint
+ "?group=" + group.uuid
+ "&loc=" + redirectLocation;
launcher.openURL(url);
return _action;
}
private PageAction openExamProctoringRoom( private PageAction openExamProctoringRoom(
final ProctoringGUIService proctoringGUIService, final ProctoringGUIService proctoringGUIService,
final ProctoringServiceSettings proctoringSettings, final ProctoringServiceSettings proctoringSettings,

View file

@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
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.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.util.Pair; import ch.ethz.seb.sebserver.gbl.util.Pair;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.RestService;
@ -40,6 +41,7 @@ public class ProctoringGUIService {
private static final Logger log = LoggerFactory.getLogger(ProctoringGUIService.class); private static final Logger log = LoggerFactory.getLogger(ProctoringGUIService.class);
public static final String SESSION_ATTR_PROCTORING_DATA = "SESSION_ATTR_PROCTORING_DATA"; public static final String SESSION_ATTR_PROCTORING_DATA = "SESSION_ATTR_PROCTORING_DATA";
public static final String SESSION_ATTR_SCREEN_PROCTORING_DATA = "SESSION_ATTR_SCREEN_PROCTORING_DATA";
private static final String SHOW_CONNECTION_ACTION_APPLIED = "SHOW_CONNECTION_ACTION_APPLIED"; 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 static final String CLOSE_ROOM_SCRIPT = "var existingWin = window.open('', '%s'); existingWin.close()";
@ -47,10 +49,23 @@ public class ProctoringGUIService {
final Map<String, RoomData> openWindows = new HashMap<>(); final Map<String, RoomData> openWindows = new HashMap<>();
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> collectingRoomsActionState; final Map<String, Pair<RemoteProctoringRoom, TreeItem>> collectingRoomsActionState;
final Map<String, TreeItem> screenProctoringGroupState;
public ProctoringGUIService(final RestService restService) { public ProctoringGUIService(final RestService restService) {
this.restService = restService; this.restService = restService;
this.collectingRoomsActionState = new HashMap<>(); this.collectingRoomsActionState = new HashMap<>();
this.screenProctoringGroupState = new HashMap<>();
}
public void registerScreeProcotringGroupAction(
final ScreenProctoringGroup screenProctoringGroup,
final TreeItem actionItem) {
this.screenProctoringGroupState.put(screenProctoringGroup.uuid, actionItem);
}
public TreeItem getScreeProcotringGroupAction(final ScreenProctoringGroup screenProctoringGroup) {
return this.screenProctoringGroupState.get(screenProctoringGroup.uuid);
} }
public boolean collectingRoomActionActive(final String name) { public boolean collectingRoomActionActive(final String name) {

View file

@ -90,20 +90,35 @@ public interface ExamAdminService {
/** This indicates if proctoring is set and enabled for a certain exam. /** This indicates if proctoring is set and enabled for a certain exam.
* *
* @param examId the exam instance * @param examId the exam instance
* @return Result refer to proctoring is enabled flag or to an error when happened. */ * @return proctoring is enabled flag */
default Result<Boolean> isProctoringEnabled(final Exam exam) { default boolean isProctoringEnabled(final Exam exam) {
if (exam == null || exam.id == null) { if (exam == null || exam.id == null) {
return Result.ofRuntimeError("Invalid Exam model"); return false;
} }
if (exam.additionalAttributesIncluded()) { if (exam.additionalAttributesIncluded()) {
return Result.tryCatch(() -> { return BooleanUtils.toBoolean(
return BooleanUtils.toBooleanObject( exam.getAdditionalAttribute(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING));
exam.getAdditionalAttribute(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING));
});
} }
return isProctoringEnabled(exam.id); return isProctoringEnabled(exam.id).getOr(false);
}
/** This indicates if screen proctoring is set and enabled for a certain exam.
*
* @param examId the exam instance
* @return screen proctoring is enabled flag */
default boolean isScreenProctoringEnabled(final Exam exam) {
if (exam == null || exam.id == null) {
return false;
}
if (exam.additionalAttributesIncluded()) {
return BooleanUtils.toBoolean(
exam.getAdditionalAttribute(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING));
}
return isProctoringEnabled(exam.id).getOr(false);
} }
/** Updates needed additional attributes from assigned exam configuration for the exam /** Updates needed additional attributes from assigned exam configuration for the exam
@ -117,6 +132,12 @@ public interface ExamAdminService {
* @return Result refer to proctoring is enabled flag or to an error when happened. */ * @return Result refer to proctoring is enabled flag or to an error when happened. */
Result<Boolean> isProctoringEnabled(final Long examId); Result<Boolean> isProctoringEnabled(final Long examId);
/** This indicates if screen proctoring is set and enabled for a certain exam.
*
* @param examId the exam identifier
* @return Result refer to screen proctoring is enabled flag or to an error when happened. */
Result<Boolean> isScreenProctoringEnabled(final Long examId);
/** Get the exam proctoring service implementation for specified exam. /** Get the exam proctoring service implementation for specified exam.
* *
* @param examId the exam identifier * @param examId the exam identifier

View file

@ -33,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction;
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.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
@ -269,6 +270,23 @@ public class ExamAdminServiceImpl implements ExamAdminService {
}); });
} }
@Override
public Result<Boolean> isScreenProctoringEnabled(final Long examId) {
return this.additionalAttributesDAO.getAdditionalAttribute(
EntityType.EXAM,
examId,
ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING)
.map(rec -> BooleanUtils.toBoolean(rec.getValue()))
.onErrorDo(error -> {
if (log.isDebugEnabled()) {
log.warn("Failed to verify screen proctoring enabled for exam: {}, {}",
examId,
error.getMessage());
}
return false;
});
}
@Override @Override
public Result<RemoteProctoringService> getExamProctoringService(final Long examId) { public Result<RemoteProctoringService> getExamProctoringService(final Long examId) {
return getProctoringServiceSettings(examId) return getProctoringServiceSettings(examId)

View file

@ -8,10 +8,13 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session; package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.util.Collection;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
@ -44,6 +47,12 @@ public interface ScreenProctoringService extends SessionUpdateTask {
* @return Result refer to the given Exam or to an error when happened */ * @return Result refer to the given Exam or to an error when happened */
Result<Exam> applyScreenProctoingForExam(Long examId); Result<Exam> applyScreenProctoingForExam(Long examId);
/** Get list of all screen proctoring collecting groups for a particular exam.
*
* @param examId The exam identifier (PK)
* @return Result refer to the list of ScreenProctoringGroup or to an error when happened */
Result<Collection<ScreenProctoringGroup>> getCollectingGroups(Long examId);
/** Gets invoked after an exam has been changed and saved. /** Gets invoked after an exam has been changed and saved.
* *
* @param exam the exam that has been changed and saved */ * @param exam the exam that has been changed and saved */

View file

@ -78,7 +78,7 @@ public class ExamSessionControlTask implements DisposableBean {
this.examTimePrefix, this.examTimePrefix,
this.examTimeSuffix); this.examTimeSuffix);
this.updateMaster(); this.webserviceInfo.updateMaster();
SEBServerInit.INIT_LOGGER.info("------>"); SEBServerInit.INIT_LOGGER.info("------>");
SEBServerInit.INIT_LOGGER.info( SEBServerInit.INIT_LOGGER.info(
@ -105,8 +105,7 @@ public class ExamSessionControlTask implements DisposableBean {
initialDelay = 5000) initialDelay = 5000)
private void examSessionUpdateTask() { private void examSessionUpdateTask() {
updateMaster(); this.webserviceInfo.updateMaster();
if (!this.webserviceInfo.isMaster()) { if (!this.webserviceInfo.isMaster()) {
return; return;
} }
@ -120,10 +119,6 @@ public class ExamSessionControlTask implements DisposableBean {
.forEach(SessionUpdateTask::processSessionUpdateTask); .forEach(SessionUpdateTask::processSessionUpdateTask);
} }
private void updateMaster() {
this.webserviceInfo.updateMaster();
}
@Override @Override
public void destroy() { public void destroy() {
// TODO try to reset master // TODO try to reset master

View file

@ -199,6 +199,11 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
}); });
} }
@Override
public Result<Collection<ScreenProctoringGroup>> getCollectingGroups(final Long examId) {
return this.screenProctoringGroupDAO.getCollectingGroups(examId);
}
@Override @Override
public Result<Exam> updateExamOnScreenProctoingService(final Long examId) { public Result<Exam> updateExamOnScreenProctoingService(final Long examId) {
return this.examDAO.byPK(examId) return this.examDAO.byPK(examId)

View file

@ -60,7 +60,6 @@ import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
@ -108,10 +107,6 @@ public class ZoomProctoringService implements RemoteProctoringService {
private static final String ZOOM_ACCESS_TOKEN_HEADER = private static final String ZOOM_ACCESS_TOKEN_HEADER =
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
@Deprecated
private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD =
"{\"iss\":\"%s\",\"exp\":%s}";
private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList( private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList(
new Tuple<>( new Tuple<>(
API.EXAM_PROCTORING_ATTR_RECEIVE_AUDIO, API.EXAM_PROCTORING_ATTR_RECEIVE_AUDIO,
@ -137,7 +132,6 @@ public class ZoomProctoringService implements RemoteProctoringService {
.stream().collect(Collectors.toMap(Tuple::get_1, Tuple::get_2))); .stream().collect(Collectors.toMap(Tuple::get_1, Tuple::get_2)));
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
private final Cryptor cryptor; private final Cryptor cryptor;
private final AsyncService asyncService; private final AsyncService asyncService;
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
@ -150,7 +144,6 @@ public class ZoomProctoringService implements RemoteProctoringService {
public ZoomProctoringService( public ZoomProctoringService(
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final Cryptor cryptor, final Cryptor cryptor,
final AsyncService asyncService, final AsyncService asyncService,
final JSONMapper jsonMapper, final JSONMapper jsonMapper,
@ -162,7 +155,6 @@ public class ZoomProctoringService implements RemoteProctoringService {
@Value("${sebserver.webservice.proctoring.zoom.tokenexpiry.seconds:86400}") final int tokenExpirySeconds) { @Value("${sebserver.webservice.proctoring.zoom.tokenexpiry.seconds:86400}") final int tokenExpirySeconds) {
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.cryptor = cryptor; this.cryptor = cryptor;
this.asyncService = asyncService; this.asyncService = asyncService;
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
@ -706,13 +698,7 @@ public class ZoomProctoringService implements RemoteProctoringService {
} }
private ZoomRestTemplate createNewRestTemplate(final ProctoringServiceSettings proctoringSettings) { private ZoomRestTemplate createNewRestTemplate(final ProctoringServiceSettings proctoringSettings) {
if (StringUtils.isNoneBlank(proctoringSettings.accountId)) { return new OAuthZoomRestTemplate(this, proctoringSettings);
log.info("Create new OAuthZoomRestTemplate for settings: {}", proctoringSettings);
return new OAuthZoomRestTemplate(this, proctoringSettings);
} else {
log.warn("Create new JWTZoomRestTemplate for settings: {}", proctoringSettings);
return new JWTZoomRestTemplate(this, proctoringSettings);
}
} }
private static abstract class ZoomRestTemplate { private static abstract class ZoomRestTemplate {
@ -1054,94 +1040,6 @@ public class ZoomProctoringService implements RemoteProctoringService {
} }
} }
@Deprecated
private final static class JWTZoomRestTemplate extends ZoomRestTemplate {
public JWTZoomRestTemplate(
final ZoomProctoringService zoomProctoringService,
final ProctoringServiceSettings proctoringSettings) {
super(zoomProctoringService, proctoringSettings);
}
@Override
public void initConnection() {
if (this.restTemplate == null) {
this.credentials = new ClientCredentials(
this.proctoringSettings.appKey,
this.proctoringSettings.appSecret);
this.restTemplate = new RestTemplate(this.zoomProctoringService.clientHttpRequestFactoryService
.getClientHttpRequestFactory()
.getOrThrow());
}
}
@Override
public HttpHeaders getHeaders() {
final String jwt = this.createJWTForAPIAccess(
this.credentials,
System.currentTimeMillis() + Constants.MINUTE_IN_MILLIS);
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt);
httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
return httpHeaders;
}
private String createJWTForAPIAccess(
final ClientCredentials credentials,
final Long expTime) {
try {
final CharSequence decryptedSecret = this.zoomProctoringService.cryptor
.decrypt(credentials.secret)
.getOrThrow();
final StringBuilder builder = new StringBuilder();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String jwtHeaderPart = urlEncoder
.encodeToString(ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format(
ZOOM_API_ACCESS_TOKEN_PAYLOAD
.replaceAll(" ", "")
.replaceAll("\n", ""),
credentials.clientIdAsString(),
expTime);
if (log.isTraceEnabled()) {
log.trace("Zoom API Token payload: {}", jwtPayload);
}
final String jwtPayloadPart = urlEncoder
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
final String message = jwtHeaderPart + "." + jwtPayloadPart;
final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG);
final SecretKeySpec secret_key = new SecretKeySpec(
Utils.toByteArray(decryptedSecret),
TOKEN_ENCODE_ALG);
sha256_HMAC.init(secret_key);
final String hash = urlEncoder
.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message)));
builder.append(message)
.append(".")
.append(hash);
return builder.toString();
} catch (final Exception e) {
throw new RuntimeException("Failed to create JWT for Zoom API access: ", e);
}
}
}
private static final class ZoomCredentialsAccessTokenProvider extends OAuth2AccessTokenSupport private static final class ZoomCredentialsAccessTokenProvider extends OAuth2AccessTokenSupport
implements AccessTokenProvider { implements AccessTokenProvider {

View file

@ -52,6 +52,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData;
@ -66,11 +67,12 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService; import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@WebServiceProfile @WebServiceProfile
@ -90,6 +92,7 @@ public class ExamMonitoringController {
private final RemoteProctoringRoomService examProcotringRoomService; private final RemoteProctoringRoomService examProcotringRoomService;
private final ExamAdminService examAdminService; private final ExamAdminService examAdminService;
private final SecurityKeyService securityKeyService; private final SecurityKeyService securityKeyService;
private final ScreenProctoringService screenProctoringService;
private final Executor executor; private final Executor executor;
public ExamMonitoringController( public ExamMonitoringController(
@ -101,6 +104,7 @@ public class ExamMonitoringController {
final RemoteProctoringRoomService examProcotringRoomService, final RemoteProctoringRoomService examProcotringRoomService,
final SecurityKeyService securityKeyService, final SecurityKeyService securityKeyService,
final ExamAdminService examAdminService, final ExamAdminService examAdminService,
final ScreenProctoringService screenProctoringService,
@Qualifier(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) final Executor executor) { @Qualifier(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) final Executor executor) {
this.sebClientConnectionService = sebClientConnectionService; this.sebClientConnectionService = sebClientConnectionService;
@ -112,6 +116,7 @@ public class ExamMonitoringController {
this.examProcotringRoomService = examProcotringRoomService; this.examProcotringRoomService = examProcotringRoomService;
this.examAdminService = examAdminService; this.examAdminService = examAdminService;
this.securityKeyService = securityKeyService; this.securityKeyService = securityKeyService;
this.screenProctoringService = screenProctoringService;
this.executor = executor; this.executor = executor;
} }
@ -314,6 +319,9 @@ public class ExamMonitoringController {
name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER, name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER,
required = false) final String hiddenClientGroups) { required = false) final String hiddenClientGroups) {
// TODO respond this within another Thread-pool (Executor)
// TODO try to cache some monitoring data throughout multiple requests (for about 2 sec.)
final Exam runningExam = checkPrivileges(institutionId, examId); final Exam runningExam = checkPrivileges(institutionId, examId);
final MonitoringSEBConnectionData monitoringSEBConnectionData = this.examSessionService final MonitoringSEBConnectionData monitoringSEBConnectionData = this.examSessionService
@ -322,25 +330,28 @@ public class ExamMonitoringController {
createMonitoringFilter(hiddenStates, hiddenClientGroups)) createMonitoringFilter(hiddenStates, hiddenClientGroups))
.getOrThrow(); .getOrThrow();
MonitoringFullPageData monitoringFullPageData; final boolean proctoringEnabled = this.examAdminService.isProctoringEnabled(runningExam);
if (this.examAdminService.isProctoringEnabled(runningExam).getOr(false)) { final boolean screenProctoringEnabled = this.examAdminService.isScreenProctoringEnabled(runningExam);
final Collection<RemoteProctoringRoom> proctoringData = this.examProcotringRoomService
.getProctoringCollectingRooms(examId)
.getOrThrow();
monitoringFullPageData = new MonitoringFullPageData( final Collection<RemoteProctoringRoom> proctoringData = (proctoringEnabled)
examId, ? this.examProcotringRoomService
monitoringSEBConnectionData, .getProctoringCollectingRooms(examId)
proctoringData); .onError(error -> log.error("Failed to get RemoteProctoringRoom for exam: {}", examId, error))
.getOr(Collections.emptyList())
: Collections.emptyList();
} else { final Collection<ScreenProctoringGroup> screenProctoringData = (screenProctoringEnabled)
monitoringFullPageData = new MonitoringFullPageData( ? this.screenProctoringService
examId, .getCollectingGroups(examId)
monitoringSEBConnectionData, .onError(error -> log.error("Failed to get ScreenProctoringGroup for exam: {}", examId, error))
Collections.emptyList()); .getOr(Collections.emptyList())
} : Collections.emptyList();
return monitoringFullPageData; return new MonitoringFullPageData(
examId,
monitoringSEBConnectionData,
proctoringData,
screenProctoringData);
} }
@RequestMapping( @RequestMapping(

View file

@ -31,12 +31,14 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
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.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@WebServiceProfile @WebServiceProfile
@ -51,17 +53,20 @@ public class ExamProctoringController {
private final ExamAdminService examAdminService; private final ExamAdminService examAdminService;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final ScreenProctoringService screenProctoringService;
public ExamProctoringController( public ExamProctoringController(
final RemoteProctoringRoomService examProcotringRoomService, final RemoteProctoringRoomService examProcotringRoomService,
final ExamAdminService examAdminService, final ExamAdminService examAdminService,
final AuthorizationService authorizationService, final AuthorizationService authorizationService,
final ExamSessionService examSessionService) { final ExamSessionService examSessionService,
final ScreenProctoringService screenProctoringService) {
this.examProcotringRoomService = examProcotringRoomService; this.examProcotringRoomService = examProcotringRoomService;
this.examAdminService = examAdminService; this.examAdminService = examAdminService;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.screenProctoringService = screenProctoringService;
} }
/** This is called by Spring to initialize the WebDataBinder and is used here to /** This is called by Spring to initialize the WebDataBinder and is used here to
@ -95,6 +100,25 @@ public class ExamProctoringController {
.getOrThrow(); .getOrThrow();
} }
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_SCREEN_PROCTORING_GROUPS_SEGMENT,
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public Collection<ScreenProctoringGroup> getScreenProctoringGroupsOfExam(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_MODEL_ID) final Long examId) {
checkAccess(institutionId, examId);
return this.screenProctoringService
.getCollectingGroups(examId)
.getOrThrow();
}
@RequestMapping( @RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT, path = API.MODEL_ID_VAR_PATH_SEGMENT,
method = RequestMethod.GET, method = RequestMethod.GET,

View file

@ -2173,14 +2173,17 @@ sebserver.monitoring.exam.list.title=Running Exams
sebserver.monitoring.exam.list.actions= sebserver.monitoring.exam.list.actions=
sebserver.monitoring.exam.action.detail.view=Back To Monitoring sebserver.monitoring.exam.action.detail.view=Back To Monitoring
sebserver.monitoring.exam.action.list.view=Monitoring sebserver.monitoring.exam.action.list.view=Monitoring
sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} ) sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} )
sebserver.monitoring.exam.action.viewgroup=View {0} ( {1} )
sebserver.exam.monitoring.action.category.statefilter=State Filter sebserver.exam.monitoring.action.category.statefilter=State Filter
sebserver.exam.monitoring.action.category.groupfilter=Client Group Filter sebserver.exam.monitoring.action.category.groupfilter=Client Group Filter
sebserver.exam.overall.action.category.proctoring=Proctoring sebserver.exam.overall.action.category.proctoring=Live Proctoring
sebserver.monitoring.exam.action.proctoring.openTownhall=Open Townhall 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.exam.overall.action.category.screenproctoring=Screen Proctoring
sebserver.monitoring.exam.proctoring.room.all.name=Townhall 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