SEBSERV-435 gui and monitoring implementation
This commit is contained in:
parent
1cbc97ef8f
commit
a04c111b59
21 changed files with 524 additions and 246 deletions
|
@ -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";
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue