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_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_CLOSE_ROOM_SEGMENT = "/close";
|
||||
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.session.RemoteProctoringRoom;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class MonitoringFullPageData {
|
||||
|
||||
public static final String ATTR_CONNECTIONS_DATA = "monitoringConnectionData";
|
||||
public static final String ATTR_PROCTORING_DATA = "proctoringData";
|
||||
public static final String ATTR_SCREEN_PROCTORING_DATA = "screenProctoringData";
|
||||
|
||||
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID)
|
||||
public final Long examId;
|
||||
|
@ -28,15 +30,19 @@ public class MonitoringFullPageData {
|
|||
public final MonitoringSEBConnectionData monitoringConnectionData;
|
||||
@JsonProperty(ATTR_PROCTORING_DATA)
|
||||
public final Collection<RemoteProctoringRoom> proctoringData;
|
||||
@JsonProperty(ATTR_SCREEN_PROCTORING_DATA)
|
||||
final Collection<ScreenProctoringGroup> screenProctoringData;
|
||||
|
||||
public MonitoringFullPageData(
|
||||
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId,
|
||||
@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.monitoringConnectionData = monitoringConnectionData;
|
||||
this.proctoringData = proctoringData;
|
||||
this.screenProctoringData = screenProctoringData;
|
||||
}
|
||||
|
||||
public Long getExamId() {
|
||||
|
@ -51,6 +57,10 @@ public class MonitoringFullPageData {
|
|||
return this.proctoringData;
|
||||
}
|
||||
|
||||
public Collection<ScreenProctoringGroup> getScreenProctoringData() {
|
||||
return this.screenProctoringData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
|
@ -79,12 +89,14 @@ public class MonitoringFullPageData {
|
|||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
builder.append("OverallMonitroingData [examId=");
|
||||
builder.append("MonitoringFullPageData [examId=");
|
||||
builder.append(this.examId);
|
||||
builder.append(", monitoringConnectionData=");
|
||||
builder.append(this.monitoringConnectionData);
|
||||
builder.append(", proctoringData=");
|
||||
builder.append(this.proctoringData);
|
||||
builder.append(", screenProctoringData=");
|
||||
builder.append(this.screenProctoringData);
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
|
|
|
@ -45,6 +45,9 @@ public class RAPSpringConfig {
|
|||
@Value("${sebserver.gui.remote.proctoring.api-servler.endpoint:/remote-view-servlet}")
|
||||
private String remoteProctoringViewServletEndpoint;
|
||||
|
||||
@Value("${sebserver.gui.screen.proctoring.api-servler.endpoint:/screen-proctoring}")
|
||||
private String screenProctoringViewServletEndpoint;
|
||||
|
||||
@Bean
|
||||
public StaticApplicationPropertyResolver staticApplicationPropertyResolver() {
|
||||
return new StaticApplicationPropertyResolver();
|
||||
|
@ -83,6 +86,17 @@ public class RAPSpringConfig {
|
|||
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
|
||||
public MessageSource messageSource() {
|
||||
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),
|
||||
GROUP_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.groupfilter"), 50),
|
||||
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);
|
||||
|
||||
|
|
|
@ -1035,11 +1035,11 @@ public enum ActionDefinition {
|
|||
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
|
||||
ActionCategory.CLIENT_EVENT_LIST),
|
||||
|
||||
MONITOR_EXAM_NEW_PROCTOR_ROOM(
|
||||
new LocTextKey("sebserver.monitoring.exam.action.newroom"),
|
||||
ImageIcon.VISIBILITY,
|
||||
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
|
||||
ActionCategory.PROCTORING),
|
||||
// MONITOR_EXAM_NEW_PROCTOR_ROOM(
|
||||
// new LocTextKey("sebserver.monitoring.exam.action.newroom"),
|
||||
// ImageIcon.VISIBILITY,
|
||||
// PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
|
||||
// ActionCategory.PROCTORING),
|
||||
MONITOR_EXAM_VIEW_PROCTOR_ROOM(
|
||||
new LocTextKey("sebserver.monitoring.exam.action.viewroom"),
|
||||
ImageIcon.PROCTOR_ROOM,
|
||||
|
@ -1056,6 +1056,12 @@ public enum ActionDefinition {
|
|||
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
|
||||
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(
|
||||
new LocTextKey("sebserver.finished.action.list"),
|
||||
PageStateDefinitionImpl.FINISHED_EXAM_LIST),
|
||||
|
|
|
@ -76,8 +76,6 @@ public class ScreenProctoringSettingsPopup {
|
|||
new LocTextKey("sebserver.exam.sps.form.accountId");
|
||||
private final static LocTextKey FORM_ACCOUNT_SECRET_SPS =
|
||||
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 =
|
||||
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.Function;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.layout.GridData;
|
||||
import org.eclipse.swt.layout.GridLayout;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.TreeItem;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
@ -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.ProctoringServiceSettings;
|
||||
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.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.UserRole;
|
||||
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.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.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.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.session.ClientConnectionTable;
|
||||
import ch.ethz.seb.sebserver.gui.service.session.FullPageMonitoringGUIUpdate;
|
||||
|
@ -76,6 +85,8 @@ import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService
|
|||
@GuiProfile
|
||||
public class MonitoringRunningExam implements TemplateComposer {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MonitoringRunningExam.class);
|
||||
|
||||
private static final LocTextKey EMPTY_SELECTION_TEXT_KEY =
|
||||
new LocTextKey("sebserver.monitoring.exam.connection.emptySelection");
|
||||
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_QUIT_SELECTED,
|
||||
ActionDefinition.MONITOR_EXAM_LOCK_SELECTED,
|
||||
ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION,
|
||||
ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM));
|
||||
ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION));
|
||||
|
||||
actionBuilder
|
||||
|
||||
|
@ -275,14 +285,19 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
.call()
|
||||
.getOr(null);
|
||||
|
||||
if (proctoringSettings != null && proctoringSettings.enableProctoring) {
|
||||
guiUpdates.add(createProctoringActions(
|
||||
proctoringSettings,
|
||||
currentUser.getProctoringGUIService(),
|
||||
pageContext,
|
||||
content,
|
||||
actionBuilder));
|
||||
}
|
||||
final ScreenProctoringSettings screenProctoringSettings = this.restService
|
||||
.getBuilder(GetScreenProctoringSettings.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||
.call()
|
||||
.getOr(null);
|
||||
|
||||
guiUpdates.add(createProctoringActions(
|
||||
proctoringSettings,
|
||||
screenProctoringSettings,
|
||||
currentUser.getProctoringGUIService(),
|
||||
pageContext,
|
||||
content));
|
||||
|
||||
}
|
||||
|
||||
// finally start the page update (server push)
|
||||
|
@ -304,12 +319,20 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
|
||||
private FullPageMonitoringGUIUpdate createProctoringActions(
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
final ScreenProctoringSettings screenProctoringSettings,
|
||||
final ProctoringGUIService proctoringGUIService,
|
||||
final PageContext pageContext,
|
||||
final Composite parent,
|
||||
final PageActionBuilder actionBuilder) {
|
||||
final Composite parent) {
|
||||
|
||||
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();
|
||||
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)
|
||||
.withEntityKey(entityKey)
|
||||
|
@ -338,18 +361,44 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
}
|
||||
}
|
||||
|
||||
this.monitoringProctoringService.initCollectingRoomActions(
|
||||
pageContext,
|
||||
actionBuilder,
|
||||
proctoringSettings,
|
||||
proctoringGUIService);
|
||||
proctoringGUIService.clearCollectingRoomActionState();
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
final Collection<RemoteProctoringRoom> collectingRooms = (proctoringEnabled)
|
||||
? 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())
|
||||
: Collections.emptyList();
|
||||
|
||||
return monitoringStatus -> this.monitoringProctoringService.updateCollectingRoomActions(
|
||||
monitoringStatus.proctoringData(),
|
||||
final Collection<ScreenProctoringGroup> screenProctoringGroups = (screenProctoringEnabled)
|
||||
? 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,
|
||||
actionBuilder,
|
||||
proctoringSettings,
|
||||
proctoringGUIService);
|
||||
proctoringGUIService,
|
||||
screenProctoringSettings);
|
||||
|
||||
return monitoringStatus -> this.monitoringProctoringService
|
||||
.updateCollectingRoomActions(
|
||||
monitoringStatus.proctoringData(),
|
||||
monitoringStatus.screenProctoringData(),
|
||||
pageContext,
|
||||
proctoringSettings,
|
||||
proctoringGUIService,
|
||||
screenProctoringSettings);
|
||||
}
|
||||
|
||||
private FullPageMonitoringGUIUpdate createFilterActions(
|
||||
|
@ -430,8 +479,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
.noEventPropagation()
|
||||
.withSwitchAction(
|
||||
actionBuilder.newAction(hideActionDef)
|
||||
.withExec(
|
||||
hideStateViewAction(filter, clientTable, status))
|
||||
.withExec(hideStateViewAction(filter, clientTable, status))
|
||||
.noEventPropagation()
|
||||
.withNameAttributes(numOfConnections)
|
||||
.create())
|
||||
|
@ -443,8 +491,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
.noEventPropagation()
|
||||
.withSwitchAction(
|
||||
actionBuilder.newAction(showActionDef)
|
||||
.withExec(
|
||||
showStateViewAction(filter, clientTable, status))
|
||||
.withExec(showStateViewAction(filter, clientTable, status))
|
||||
.noEventPropagation()
|
||||
.withNameAttributes(numOfConnections)
|
||||
.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.ClientMonitoringData;
|
||||
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.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;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
@ -18,6 +18,7 @@ import org.apache.commons.lang3.BooleanUtils;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.rap.rwt.RWT;
|
||||
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.Image;
|
||||
import org.eclipse.swt.widgets.Display;
|
||||
|
@ -27,7 +28,10 @@ import org.slf4j.LoggerFactory;
|
|||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
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.ProctoringServiceSettings;
|
||||
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.RemoteProctoringRoom;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Tuple;
|
||||
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.event.ActionActivationEvent;
|
||||
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.IsTownhallRoomAvailable;
|
||||
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 String remoteProctoringEndpoint;
|
||||
|
||||
@Value("${sebserver.gui.screen.proctoring.api-servler.endpoint:/screen-proctoring}")
|
||||
private String screenProctoringViewServletEndpoint;
|
||||
|
||||
public MonitoringProctoringService(
|
||||
final PageService pageService,
|
||||
final GuiServiceInfo guiServiceInfo,
|
||||
|
@ -152,87 +160,124 @@ public class MonitoringProctoringService {
|
|||
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(
|
||||
final Collection<RemoteProctoringRoom> collectingRooms,
|
||||
final Collection<ScreenProctoringGroup> screenProctoringGroups,
|
||||
final PageContext pageContext,
|
||||
final PageActionBuilder actionBuilder,
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
final ProctoringGUIService proctoringGUIService) {
|
||||
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
final I18nSupport i18nSupport = this.pageService.getI18nSupport();
|
||||
final ProctoringGUIService proctoringGUIService,
|
||||
final ScreenProctoringSettings screenProctoringSettings) {
|
||||
|
||||
collectingRooms
|
||||
.stream()
|
||||
.forEach(room -> {
|
||||
if (proctoringGUIService.collectingRoomActionActive(room.name)) {
|
||||
// update action
|
||||
final TreeItem treeItem = proctoringGUIService.getCollectingRoomActionItem(room.name);
|
||||
proctoringGUIService.registerCollectingRoomAction(room, treeItem);
|
||||
treeItem.setText(i18nSupport.getText(new LocTextKey(
|
||||
ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM.title.name,
|
||||
room.subject,
|
||||
room.roomSize,
|
||||
proctoringSettings.collectingRoomSize)));
|
||||
processProctorRoomActionActivation(treeItem, room, pageContext);
|
||||
} else {
|
||||
// create new action
|
||||
final PageAction action =
|
||||
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(_action -> 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);
|
||||
}
|
||||
});
|
||||
.forEach(room -> updateProctoringAction(
|
||||
pageContext,
|
||||
proctoringSettings,
|
||||
proctoringGUIService,
|
||||
room));
|
||||
|
||||
if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) {
|
||||
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(
|
||||
|
@ -263,6 +308,28 @@ public class MonitoringProctoringService {
|
|||
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(
|
||||
final ProctoringGUIService proctoringGUIService,
|
||||
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.model.exam.ProctoringRoomConnection;
|
||||
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.Result;
|
||||
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);
|
||||
|
||||
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 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, Pair<RemoteProctoringRoom, TreeItem>> collectingRoomsActionState;
|
||||
final Map<String, TreeItem> screenProctoringGroupState;
|
||||
|
||||
public ProctoringGUIService(final RestService restService) {
|
||||
this.restService = restService;
|
||||
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) {
|
||||
|
|
|
@ -90,20 +90,35 @@ public interface ExamAdminService {
|
|||
/** This indicates if proctoring is set and enabled for a certain exam.
|
||||
*
|
||||
* @param examId the exam instance
|
||||
* @return Result refer to proctoring is enabled flag or to an error when happened. */
|
||||
default Result<Boolean> isProctoringEnabled(final Exam exam) {
|
||||
* @return proctoring is enabled flag */
|
||||
default boolean isProctoringEnabled(final Exam exam) {
|
||||
if (exam == null || exam.id == null) {
|
||||
return Result.ofRuntimeError("Invalid Exam model");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exam.additionalAttributesIncluded()) {
|
||||
return Result.tryCatch(() -> {
|
||||
return BooleanUtils.toBooleanObject(
|
||||
exam.getAdditionalAttribute(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING));
|
||||
});
|
||||
return BooleanUtils.toBoolean(
|
||||
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
|
||||
|
@ -117,6 +132,12 @@ public interface ExamAdminService {
|
|||
* @return Result refer to proctoring is enabled flag or to an error when happened. */
|
||||
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.
|
||||
*
|
||||
* @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.ProctoringServiceSettings;
|
||||
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.LmsType;
|
||||
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
|
||||
public Result<RemoteProctoringService> getExamProctoringService(final Long examId) {
|
||||
return getProctoringServiceSettings(examId)
|
||||
|
|
|
@ -8,10 +8,13 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
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.session.ScreenProctoringGroup;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
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 */
|
||||
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.
|
||||
*
|
||||
* @param exam the exam that has been changed and saved */
|
||||
|
|
|
@ -78,7 +78,7 @@ public class ExamSessionControlTask implements DisposableBean {
|
|||
this.examTimePrefix,
|
||||
this.examTimeSuffix);
|
||||
|
||||
this.updateMaster();
|
||||
this.webserviceInfo.updateMaster();
|
||||
|
||||
SEBServerInit.INIT_LOGGER.info("------>");
|
||||
SEBServerInit.INIT_LOGGER.info(
|
||||
|
@ -105,8 +105,7 @@ public class ExamSessionControlTask implements DisposableBean {
|
|||
initialDelay = 5000)
|
||||
private void examSessionUpdateTask() {
|
||||
|
||||
updateMaster();
|
||||
|
||||
this.webserviceInfo.updateMaster();
|
||||
if (!this.webserviceInfo.isMaster()) {
|
||||
return;
|
||||
}
|
||||
|
@ -120,10 +119,6 @@ public class ExamSessionControlTask implements DisposableBean {
|
|||
.forEach(SessionUpdateTask::processSessionUpdateTask);
|
||||
}
|
||||
|
||||
private void updateMaster() {
|
||||
this.webserviceInfo.updateMaster();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// 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
|
||||
public Result<Exam> updateExamOnScreenProctoingService(final Long 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.util.UriComponentsBuilder;
|
||||
|
||||
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
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 =
|
||||
"{\"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(
|
||||
new Tuple<>(
|
||||
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)));
|
||||
|
||||
private final ExamSessionService examSessionService;
|
||||
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||
private final Cryptor cryptor;
|
||||
private final AsyncService asyncService;
|
||||
private final JSONMapper jsonMapper;
|
||||
|
@ -150,7 +144,6 @@ public class ZoomProctoringService implements RemoteProctoringService {
|
|||
|
||||
public ZoomProctoringService(
|
||||
final ExamSessionService examSessionService,
|
||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||
final Cryptor cryptor,
|
||||
final AsyncService asyncService,
|
||||
final JSONMapper jsonMapper,
|
||||
|
@ -162,7 +155,6 @@ public class ZoomProctoringService implements RemoteProctoringService {
|
|||
@Value("${sebserver.webservice.proctoring.zoom.tokenexpiry.seconds:86400}") final int tokenExpirySeconds) {
|
||||
|
||||
this.examSessionService = examSessionService;
|
||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||
this.cryptor = cryptor;
|
||||
this.asyncService = asyncService;
|
||||
this.jsonMapper = jsonMapper;
|
||||
|
@ -706,13 +698,7 @@ public class ZoomProctoringService implements RemoteProctoringService {
|
|||
}
|
||||
|
||||
private ZoomRestTemplate createNewRestTemplate(final ProctoringServiceSettings proctoringSettings) {
|
||||
if (StringUtils.isNoneBlank(proctoringSettings.accountId)) {
|
||||
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);
|
||||
}
|
||||
return new OAuthZoomRestTemplate(this, proctoringSettings);
|
||||
}
|
||||
|
||||
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
|
||||
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.ClientNotification;
|
||||
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.UserRole;
|
||||
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.exam.ExamAdminService;
|
||||
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.RemoteProctoringRoomService;
|
||||
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.SEBClientNotificationService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
|
||||
@WebServiceProfile
|
||||
|
@ -90,6 +92,7 @@ public class ExamMonitoringController {
|
|||
private final RemoteProctoringRoomService examProcotringRoomService;
|
||||
private final ExamAdminService examAdminService;
|
||||
private final SecurityKeyService securityKeyService;
|
||||
private final ScreenProctoringService screenProctoringService;
|
||||
private final Executor executor;
|
||||
|
||||
public ExamMonitoringController(
|
||||
|
@ -101,6 +104,7 @@ public class ExamMonitoringController {
|
|||
final RemoteProctoringRoomService examProcotringRoomService,
|
||||
final SecurityKeyService securityKeyService,
|
||||
final ExamAdminService examAdminService,
|
||||
final ScreenProctoringService screenProctoringService,
|
||||
@Qualifier(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) final Executor executor) {
|
||||
|
||||
this.sebClientConnectionService = sebClientConnectionService;
|
||||
|
@ -112,6 +116,7 @@ public class ExamMonitoringController {
|
|||
this.examProcotringRoomService = examProcotringRoomService;
|
||||
this.examAdminService = examAdminService;
|
||||
this.securityKeyService = securityKeyService;
|
||||
this.screenProctoringService = screenProctoringService;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
|
@ -314,6 +319,9 @@ public class ExamMonitoringController {
|
|||
name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER,
|
||||
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 MonitoringSEBConnectionData monitoringSEBConnectionData = this.examSessionService
|
||||
|
@ -322,25 +330,28 @@ public class ExamMonitoringController {
|
|||
createMonitoringFilter(hiddenStates, hiddenClientGroups))
|
||||
.getOrThrow();
|
||||
|
||||
MonitoringFullPageData monitoringFullPageData;
|
||||
if (this.examAdminService.isProctoringEnabled(runningExam).getOr(false)) {
|
||||
final Collection<RemoteProctoringRoom> proctoringData = this.examProcotringRoomService
|
||||
.getProctoringCollectingRooms(examId)
|
||||
.getOrThrow();
|
||||
final boolean proctoringEnabled = this.examAdminService.isProctoringEnabled(runningExam);
|
||||
final boolean screenProctoringEnabled = this.examAdminService.isScreenProctoringEnabled(runningExam);
|
||||
|
||||
monitoringFullPageData = new MonitoringFullPageData(
|
||||
examId,
|
||||
monitoringSEBConnectionData,
|
||||
proctoringData);
|
||||
final Collection<RemoteProctoringRoom> proctoringData = (proctoringEnabled)
|
||||
? this.examProcotringRoomService
|
||||
.getProctoringCollectingRooms(examId)
|
||||
.onError(error -> log.error("Failed to get RemoteProctoringRoom for exam: {}", examId, error))
|
||||
.getOr(Collections.emptyList())
|
||||
: Collections.emptyList();
|
||||
|
||||
} else {
|
||||
monitoringFullPageData = new MonitoringFullPageData(
|
||||
examId,
|
||||
monitoringSEBConnectionData,
|
||||
Collections.emptyList());
|
||||
}
|
||||
final Collection<ScreenProctoringGroup> screenProctoringData = (screenProctoringEnabled)
|
||||
? this.screenProctoringService
|
||||
.getCollectingGroups(examId)
|
||||
.onError(error -> log.error("Failed to get ScreenProctoringGroup for exam: {}", examId, error))
|
||||
.getOr(Collections.emptyList())
|
||||
: Collections.emptyList();
|
||||
|
||||
return monitoringFullPageData;
|
||||
return new MonitoringFullPageData(
|
||||
examId,
|
||||
monitoringSEBConnectionData,
|
||||
proctoringData,
|
||||
screenProctoringData);
|
||||
}
|
||||
|
||||
@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.session.ClientConnection;
|
||||
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.webservice.servicelayer.authorization.AuthorizationService;
|
||||
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.session.RemoteProctoringRoomService;
|
||||
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;
|
||||
|
||||
@WebServiceProfile
|
||||
|
@ -51,17 +53,20 @@ public class ExamProctoringController {
|
|||
private final ExamAdminService examAdminService;
|
||||
private final AuthorizationService authorizationService;
|
||||
private final ExamSessionService examSessionService;
|
||||
private final ScreenProctoringService screenProctoringService;
|
||||
|
||||
public ExamProctoringController(
|
||||
final RemoteProctoringRoomService examProcotringRoomService,
|
||||
final ExamAdminService examAdminService,
|
||||
final AuthorizationService authorizationService,
|
||||
final ExamSessionService examSessionService) {
|
||||
final ExamSessionService examSessionService,
|
||||
final ScreenProctoringService screenProctoringService) {
|
||||
|
||||
this.examProcotringRoomService = examProcotringRoomService;
|
||||
this.examAdminService = examAdminService;
|
||||
this.authorizationService = authorizationService;
|
||||
this.examSessionService = examSessionService;
|
||||
this.screenProctoringService = screenProctoringService;
|
||||
}
|
||||
|
||||
/** This is called by Spring to initialize the WebDataBinder and is used here to
|
||||
|
@ -95,6 +100,25 @@ public class ExamProctoringController {
|
|||
.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(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
|
|
|
@ -2173,14 +2173,17 @@ sebserver.monitoring.exam.list.title=Running Exams
|
|||
sebserver.monitoring.exam.list.actions=
|
||||
sebserver.monitoring.exam.action.detail.view=Back To 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.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.showTownhall=Show 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.action.close=Close Window
|
||||
sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast
|
||||
|
|
Loading…
Reference in a new issue