SEBSERV-139 added collecting all room feature
This commit is contained in:
parent
6e3549dbb7
commit
057275ba49
24 changed files with 863 additions and 561 deletions
|
@ -128,14 +128,7 @@ public final class API {
|
|||
public static final String EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction";
|
||||
public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
|
||||
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters";
|
||||
public static final String PROCTORING_PATH_SEGMENT = "/proctoring";
|
||||
public static final String PROCTORING_ROOMS_SEGMENT = "/rooms";
|
||||
public static final String PROCTORING_JOIN_ROOM_PATH_SEGMENT = "/join";
|
||||
public static final String PROCTORING_LEAVE_ROOM_PATH_SEGMENT = "/leave";
|
||||
public static final String PROCTORING_REJOIN_EXAM_ROOM_PATH_SEGMENT = "/rejoin-exam-room";
|
||||
public static final String PROCTORING_BROADCAST_ON_PATH_SEGMENT = "/broadcast-on";
|
||||
public static final String PROCTORING_BROADCAST_OFF_PATH_SEGMENT = "/broadcast-off";
|
||||
public static final String PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections";
|
||||
public static final String EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT = "/proctoring";
|
||||
|
||||
public static final String EXAM_INDICATOR_ENDPOINT = "/indicator";
|
||||
|
||||
|
@ -177,6 +170,16 @@ public final class API {
|
|||
public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT =
|
||||
"/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}";
|
||||
|
||||
public static final String EXAM_PROCTORING_ENDPOINT = EXAM_MONITORING_ENDPOINT + "/proctoring";
|
||||
public static final String EXAM_PROCTORING_ROOMS_SEGMENT = "/rooms";
|
||||
public static final String EXAM_PROCTORING_JOIN_ROOM_PATH_SEGMENT = "/join";
|
||||
public static final String EXAM_PROCTORING_REJOIN_COLLECTING_ROOM_PATH_SEGMENT = "/rejoin-collecting-room";
|
||||
public static final String EXAM_PROCTORING_BROADCAST_ON_PATH_SEGMENT = "/broadcast-on";
|
||||
public static final String EXAM_PROCTORING_BROADCAST_OFF_PATH_SEGMENT = "/broadcast-off";
|
||||
public static final String EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections";
|
||||
public static final String EXAM_PROCTORING_JON_ALL_COLLECTING_ROOM = "join-all-collecting-room";
|
||||
public static final String EXAM_PROCTORING_REJON_ALL_COLLECTING_ROOM = "rejoin-all-collecting-room";
|
||||
|
||||
public static final String SEB_CLIENT_CONNECTION_ENDPOINT = "/seb-client-connection";
|
||||
|
||||
public static final String SEB_CLIENT_EVENT_ENDPOINT = "/seb-client-event";
|
||||
|
|
|
@ -372,16 +372,25 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
final String roomName = urlEncoder.encodeToString(Utils.toByteArray(connectionToken));
|
||||
final String examId = action.getEntityKey().modelId;
|
||||
|
||||
final SEBProctoringConnectionData proctoringConnectionData = this.pageService.getCurrentUser()
|
||||
.getProctoringGUIService()
|
||||
.registerNewSingleProcotringRoom(
|
||||
examId,
|
||||
roomName,
|
||||
connectionData.clientConnection.userSessionId,
|
||||
connectionToken)
|
||||
.getOrThrow();
|
||||
final ProctoringGUIService proctoringGUIService = this.pageService
|
||||
.getCurrentUser()
|
||||
.getProctoringGUIService();
|
||||
|
||||
if (!proctoringGUIService.hasRoom(roomName)) {
|
||||
final SEBProctoringConnectionData proctoringConnectionData = proctoringGUIService
|
||||
.registerNewSingleProcotringRoom(
|
||||
examId,
|
||||
roomName,
|
||||
connectionData.clientConnection.userSessionId,
|
||||
connectionToken)
|
||||
.onError(error -> log.error(
|
||||
"Failed to open single proctoring room for connection {} {}",
|
||||
connectionToken,
|
||||
error.getMessage()))
|
||||
.getOr(null);
|
||||
ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData);
|
||||
}
|
||||
|
||||
ProctoringGUIService.setCurrentProctoringWindowData(examId, proctoringConnectionData);
|
||||
final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
|
||||
final String script = String.format(
|
||||
OPEN_SINGEL_ROOM_SCRIPT,
|
||||
|
@ -389,9 +398,7 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
|
||||
this.remoteProctoringEndpoint);
|
||||
javaScriptExecutor.execute(script);
|
||||
this.pageService.getCurrentUser()
|
||||
.getProctoringGUIService()
|
||||
.registerProctoringWindow(roomName);
|
||||
proctoringGUIService.registerProctoringWindow(roomName);
|
||||
return action;
|
||||
}
|
||||
|
||||
|
|
|
@ -108,6 +108,8 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.all.confirm");
|
||||
private static final LocTextKey CONFIRM_DISABLE_SELECTED =
|
||||
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.disable.selected.confirm");
|
||||
private static final LocTextKey EXAM_ROOM_NAME =
|
||||
new LocTextKey("sebserver.monitoring.exam.proctoring.room.all.name");
|
||||
|
||||
private final ServerPushService serverPushService;
|
||||
private final PageService pageService;
|
||||
|
@ -329,6 +331,13 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
.getOr(null);
|
||||
|
||||
if (proctoringSettings != null && proctoringSettings.enableProctoring) {
|
||||
|
||||
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_CREATE_ALL_PROCTOR_ROOM)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(this::createCollectingAllRoom)
|
||||
.noEventPropagation()
|
||||
.publish();
|
||||
|
||||
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> availableRooms = new HashMap<>();
|
||||
updateRoomActions(
|
||||
entityKey,
|
||||
|
@ -348,6 +357,41 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
}
|
||||
}
|
||||
|
||||
private PageAction createCollectingAllRoom(final PageAction action) {
|
||||
final EntityKey examId = action.getEntityKey();
|
||||
|
||||
final ProctoringGUIService proctoringGUIService = this.pageService
|
||||
.getCurrentUser()
|
||||
.getProctoringGUIService();
|
||||
|
||||
String activeAllRoomName = proctoringGUIService.getActiveAllRoom(examId.modelId);
|
||||
|
||||
if (activeAllRoomName == null) {
|
||||
final SEBProctoringConnectionData proctoringConnectionData = proctoringGUIService
|
||||
.registerAllProcotringRoom(
|
||||
examId.modelId,
|
||||
this.pageService.getI18nSupport().getText(EXAM_ROOM_NAME))
|
||||
.onError(error -> log.error(
|
||||
"Failed to open all collecting room for exam {} {}", examId.modelId, error.getMessage()))
|
||||
.getOrThrow();
|
||||
ProctoringGUIService.setCurrentProctoringWindowData(
|
||||
examId.modelId,
|
||||
proctoringConnectionData);
|
||||
activeAllRoomName = proctoringConnectionData.roomName;
|
||||
}
|
||||
|
||||
final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
|
||||
final String script = String.format(
|
||||
OPEN_EXAM_COLLECTION_ROOM_SCRIPT,
|
||||
activeAllRoomName,
|
||||
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
|
||||
this.remoteProctoringEndpoint);
|
||||
javaScriptExecutor.execute(script);
|
||||
proctoringGUIService.registerProctoringWindow(activeAllRoomName);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private void updateRoomActions(
|
||||
final EntityKey entityKey,
|
||||
final PageContext pageContext,
|
||||
|
@ -359,7 +403,8 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
this.pageService.getRestService().getBuilder(GetProcotringRooms.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||
.call()
|
||||
.getOrThrow()
|
||||
.onError(error -> log.error("Failed to update proctoring rooms on GUI {}", error.getMessage()))
|
||||
.getOr(Collections.emptyList())
|
||||
.stream()
|
||||
.forEach(room -> {
|
||||
if (rooms.containsKey(room.name)) {
|
||||
|
|
|
@ -704,6 +704,11 @@ public enum ActionDefinition {
|
|||
ImageIcon.PROCTOR_ROOM,
|
||||
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
|
||||
ActionCategory.PROCTORING),
|
||||
MONITOR_EXAM_CREATE_ALL_PROCTOR_ROOM(
|
||||
new LocTextKey("sebserver.monitoring.exam.action.proctoring.allRoom"),
|
||||
ImageIcon.PROCTOR_ROOM,
|
||||
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
|
||||
ActionCategory.PROCTORING),
|
||||
|
||||
LOGS_USER_ACTIVITY_LIST(
|
||||
new LocTextKey("sebserver.logs.activity.userlogs"),
|
||||
|
|
|
@ -36,7 +36,7 @@ public class GetProctoringSettings extends RestCall<ProctoringSettings> {
|
|||
MediaType.APPLICATION_JSON_UTF8,
|
||||
API.EXAM_ADMINISTRATION_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT);
|
||||
+ API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ public class SaveProctoringSettings extends RestCall<Exam> {
|
|||
MediaType.APPLICATION_JSON_UTF8,
|
||||
API.EXAM_ADMINISTRATION_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT);
|
||||
+ API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
|
@ -26,19 +24,19 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
|||
@Lazy
|
||||
@Component
|
||||
@GuiProfile
|
||||
public class LeaveRemoteProctoringRoom extends RestCall<List<SEBProctoringConnectionData>> {
|
||||
public class CreateCollectingAllProctoringRoom extends RestCall<SEBProctoringConnectionData> {
|
||||
|
||||
public LeaveRemoteProctoringRoom() {
|
||||
public CreateCollectingAllProctoringRoom() {
|
||||
super(new TypeKey<>(
|
||||
CallType.UNDEFINED,
|
||||
EntityType.EXAM_PROCTOR_DATA,
|
||||
new TypeReference<List<SEBProctoringConnectionData>>() {
|
||||
new TypeReference<SEBProctoringConnectionData>() {
|
||||
}),
|
||||
HttpMethod.POST,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_MONITORING_ENDPOINT
|
||||
API.EXAM_PROCTORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_LEAVE_ROOM_PATH_SEGMENT);
|
||||
+ API.EXAM_PROCTORING_JON_ALL_COLLECTING_ROOM);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 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.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
@GuiProfile
|
||||
public class DisposeCollectingAllProctoringRoom extends RestCall<Void> {
|
||||
|
||||
public DisposeCollectingAllProctoringRoom() {
|
||||
super(new TypeKey<>(
|
||||
CallType.UNDEFINED,
|
||||
EntityType.EXAM_PROCTOR_DATA,
|
||||
new TypeReference<Void>() {
|
||||
}),
|
||||
HttpMethod.POST,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_PROCTORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_PROCTORING_REJON_ALL_COLLECTING_ROOM);
|
||||
}
|
||||
|
||||
}
|
|
@ -36,9 +36,8 @@ public class GetProcotringRooms extends RestCall<Collection<RemoteProctoringRoom
|
|||
}),
|
||||
HttpMethod.GET,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_MONITORING_ENDPOINT
|
||||
API.EXAM_PROCTORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_ROOMS_SEGMENT);
|
||||
+ API.EXAM_PROCTORING_ROOMS_SEGMENT);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,8 @@ public class GetProctorRoomConnectionData extends RestCall<SEBProctoringConnecti
|
|||
}),
|
||||
HttpMethod.GET,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_MONITORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT);
|
||||
API.EXAM_PROCTORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,10 +36,9 @@ public class GetProctorRoomConnections extends RestCall<Collection<ClientConnect
|
|||
}),
|
||||
HttpMethod.GET,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_MONITORING_ENDPOINT
|
||||
API.EXAM_PROCTORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT);
|
||||
+ API.EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -34,10 +34,9 @@ public class SendJoinRemoteProctoringRoom extends RestCall<SEBProctoringConnecti
|
|||
}),
|
||||
HttpMethod.POST,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_MONITORING_ENDPOINT
|
||||
API.EXAM_PROCTORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_JOIN_ROOM_PATH_SEGMENT);
|
||||
+ API.EXAM_PROCTORING_JOIN_ROOM_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,10 +33,9 @@ public class SendProctoringBroadcastOffInstruction extends RestCall<Void> {
|
|||
}),
|
||||
HttpMethod.POST,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_MONITORING_ENDPOINT
|
||||
API.EXAM_PROCTORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_BROADCAST_OFF_PATH_SEGMENT);
|
||||
+ API.EXAM_PROCTORING_BROADCAST_OFF_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,10 +33,9 @@ public class SendProctoringBroadcastOnInstruction extends RestCall<Void> {
|
|||
}),
|
||||
HttpMethod.POST,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_MONITORING_ENDPOINT
|
||||
API.EXAM_PROCTORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_BROADCAST_ON_PATH_SEGMENT);
|
||||
+ API.EXAM_PROCTORING_BROADCAST_ON_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,10 +33,9 @@ public class SendRejoinExamCollectionRoom extends RestCall<Void> {
|
|||
}),
|
||||
HttpMethod.POST,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_MONITORING_ENDPOINT
|
||||
API.EXAM_PROCTORING_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_REJOIN_EXAM_ROOM_PATH_SEGMENT);
|
||||
+ API.EXAM_PROCTORING_REJOIN_COLLECTING_ROOM_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.util.Collection;
|
|||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
|
@ -28,6 +29,8 @@ import ch.ethz.seb.sebserver.gbl.api.API;
|
|||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.CreateCollectingAllProctoringRoom;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.DisposeCollectingAllProctoringRoom;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendJoinRemoteProctoringRoom;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendRejoinExamCollectionRoom;
|
||||
|
||||
|
@ -84,26 +87,54 @@ public class ProctoringGUIService {
|
|||
new ProctoringWindowData(examId, data));
|
||||
}
|
||||
|
||||
public boolean hasRoom(final String roomName) {
|
||||
return this.rooms.containsKey(roomName);
|
||||
}
|
||||
|
||||
public String getActiveAllRoom(final String examId) {
|
||||
return this.rooms
|
||||
.values()
|
||||
.stream()
|
||||
.filter(data -> Objects.equals(data.examId, examId) && data.connections.isEmpty())
|
||||
.findFirst()
|
||||
.map(data -> data.roomName)
|
||||
.orElseGet(() -> null);
|
||||
}
|
||||
|
||||
public Result<SEBProctoringConnectionData> registerNewSingleProcotringRoom(
|
||||
final String examId,
|
||||
final String roomName,
|
||||
final String subject,
|
||||
final String connectionToken) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final SEBProctoringConnectionData connection =
|
||||
this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, examId)
|
||||
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
|
||||
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
|
||||
.withFormParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
|
||||
.call()
|
||||
.getOrThrow();
|
||||
return this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, examId)
|
||||
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
|
||||
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
|
||||
.withFormParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
|
||||
.call()
|
||||
.map(connection -> {
|
||||
this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionToken));
|
||||
this.openWindows.add(roomName);
|
||||
return connection;
|
||||
});
|
||||
}
|
||||
|
||||
this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionToken));
|
||||
this.openWindows.add(roomName);
|
||||
return connection;
|
||||
});
|
||||
public Result<SEBProctoringConnectionData> registerAllProcotringRoom(
|
||||
final String examId,
|
||||
final String subject) {
|
||||
|
||||
return this.restService.getBuilder(CreateCollectingAllProctoringRoom.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, examId)
|
||||
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
|
||||
.call()
|
||||
.map(connection -> {
|
||||
this.rooms.put(
|
||||
connection.roomName,
|
||||
new RoomConnectionData(connection.roomName, examId));
|
||||
this.openWindows.add(connection.roomName);
|
||||
return connection;
|
||||
});
|
||||
}
|
||||
|
||||
public Result<SEBProctoringConnectionData> registerNewProcotringRoom(
|
||||
|
@ -112,22 +143,19 @@ public class ProctoringGUIService {
|
|||
final String subject,
|
||||
final Collection<String> connectionTokens) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final SEBProctoringConnectionData connection =
|
||||
this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, examId)
|
||||
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
|
||||
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
|
||||
.withFormParam(
|
||||
API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR))
|
||||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionTokens));
|
||||
this.openWindows.add(roomName);
|
||||
return connection;
|
||||
});
|
||||
return this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, examId)
|
||||
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
|
||||
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
|
||||
.withFormParam(
|
||||
API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR))
|
||||
.call()
|
||||
.map(connection -> {
|
||||
this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connectionTokens));
|
||||
this.openWindows.add(roomName);
|
||||
return connection;
|
||||
});
|
||||
}
|
||||
|
||||
public void addConnectionsToRoom(
|
||||
|
@ -147,7 +175,9 @@ public class ProctoringGUIService {
|
|||
API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR))
|
||||
.call()
|
||||
.getOrThrow();
|
||||
.onError(error -> log.error("Failed to add connection to proctoring room: {} {}",
|
||||
room,
|
||||
error.getMessage()));
|
||||
roomConnectionData.connections.addAll(connectionTokens);
|
||||
}
|
||||
}
|
||||
|
@ -164,15 +194,24 @@ public class ProctoringGUIService {
|
|||
final RoomConnectionData roomConnectionData = this.rooms.remove(name);
|
||||
if (roomConnectionData != null) {
|
||||
// send instruction to leave this room and join the own exam collection room
|
||||
|
||||
this.restService.getBuilder(SendRejoinExamCollectionRoom.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId)
|
||||
.withFormParam(
|
||||
API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
StringUtils.join(roomConnectionData.connections, Constants.LIST_SEPARATOR_CHAR))
|
||||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
if (!roomConnectionData.connections.isEmpty()) {
|
||||
this.restService.getBuilder(SendRejoinExamCollectionRoom.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId)
|
||||
.withFormParam(
|
||||
API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
StringUtils.join(roomConnectionData.connections, Constants.LIST_SEPARATOR_CHAR))
|
||||
.call()
|
||||
.onError(error -> log.error("Failed to close proctoring room: {} {}",
|
||||
name,
|
||||
error.getMessage()));
|
||||
} else {
|
||||
this.restService.getBuilder(DisposeCollectingAllProctoringRoom.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId)
|
||||
.call()
|
||||
.onError(error -> log.error("Failed to close proctoring room: {} {}",
|
||||
name,
|
||||
error.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session;
|
|||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
|
||||
public interface ExamProctoringService {
|
||||
|
@ -20,14 +21,18 @@ public interface ExamProctoringService {
|
|||
Result<Boolean> testExamProctoring(final ProctoringSettings examProctoring);
|
||||
|
||||
Result<SEBProctoringConnectionData> createProctorPublicRoomConnection(
|
||||
final ProctoringSettings examProctoring,
|
||||
final ProctoringSettings proctoringSettings,
|
||||
final String roomName,
|
||||
final String subject);
|
||||
|
||||
Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData(
|
||||
final ProctoringSettings examProctoring,
|
||||
final ProctoringSettings proctoringSettings,
|
||||
final String connectionToken);
|
||||
|
||||
Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData(
|
||||
final ProctoringSettings proctoringSettings,
|
||||
final ClientConnection connection);
|
||||
|
||||
Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData(
|
||||
final ProctoringSettings proctoringSettings,
|
||||
final String connectionToken,
|
||||
|
|
|
@ -13,6 +13,7 @@ import java.security.InvalidKeyException;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Base64.Encoder;
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
@ -26,6 +27,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
|||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
|
@ -99,15 +101,27 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
|
|||
final ProctoringSettings proctoringSettings,
|
||||
final String connectionToken) {
|
||||
|
||||
return this.examSessionService
|
||||
.getConnectionData(connectionToken)
|
||||
.flatMap(connection -> getClientExamCollectionRoomConnectionData(
|
||||
proctoringSettings,
|
||||
connection.clientConnection));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<SEBProctoringConnectionData> getClientExamCollectionRoomConnectionData(
|
||||
final ProctoringSettings proctoringSettings,
|
||||
final ClientConnection connection) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final ClientConnectionData clientConnection = this.examSessionService
|
||||
.getConnectionData(connectionToken)
|
||||
.getOrThrow();
|
||||
final RemoteProctoringRoom room = this.examSessionService
|
||||
|
||||
final Collection<RemoteProctoringRoom> remoteProctoringRooms = this.examSessionService
|
||||
.getExamSessionCacheService()
|
||||
.getRemoteProctoringRooms(clientConnection.clientConnection.examId)
|
||||
.getRemoteProctoringRooms(connection.examId);
|
||||
|
||||
final RemoteProctoringRoom room = remoteProctoringRooms
|
||||
.stream()
|
||||
.filter(r -> r.id.equals(clientConnection.clientConnection.remoteProctoringRoomId))
|
||||
.filter(r -> r.id.equals(connection.remoteProctoringRoomId))
|
||||
.findFirst()
|
||||
.orElseGet(() -> {
|
||||
throw new RuntimeException("No exam proctoring room found for clientConnection");
|
||||
|
@ -119,10 +133,10 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
|
|||
proctoringSettings.serverURL,
|
||||
proctoringSettings.appKey,
|
||||
proctoringSettings.getAppSecret(),
|
||||
clientConnection.clientConnection.userSessionId,
|
||||
connection.userSessionId,
|
||||
"seb-client",
|
||||
room.name,
|
||||
clientConnection.clientConnection.userSessionId,
|
||||
connection.userSessionId,
|
||||
forExam(proctoringSettings))
|
||||
.getOrThrow();
|
||||
});
|
||||
|
|
|
@ -124,7 +124,8 @@ public class ExamProcotringRoomServiceImpl implements ExamProcotringRoomService
|
|||
.build()
|
||||
.execute();
|
||||
|
||||
flagUpdated(toUpdate).stream()
|
||||
flagUpdated(toUpdate)
|
||||
.stream()
|
||||
.forEach(cc -> {
|
||||
if (ConnectionStatus.ACTIVE.name().equals(cc.getStatus())) {
|
||||
assignToRoom(cc);
|
||||
|
@ -156,13 +157,14 @@ public class ExamProcotringRoomServiceImpl implements ExamProcotringRoomService
|
|||
ACTIVE_COLLECTING_ALL_ROOM_ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
private Result<String> getActiveCollectingAllRoom(final Long examId) {
|
||||
private String getActiveCollectingAllRoom(final Long examId) {
|
||||
return this.additionalAttributesDAO
|
||||
.getAdditionalAttribute(
|
||||
EntityType.EXAM,
|
||||
examId,
|
||||
ACTIVE_COLLECTING_ALL_ROOM_ATTRIBUTE_NAME)
|
||||
.map(attr -> attr.getValue());
|
||||
.map(attr -> attr.getValue())
|
||||
.getOr(null);
|
||||
}
|
||||
|
||||
// TODO considering doing bulk update here
|
||||
|
@ -202,7 +204,13 @@ public class ExamProcotringRoomServiceImpl implements ExamProcotringRoomService
|
|||
proctoringRoom.id,
|
||||
0));
|
||||
this.examSessionCacheService.evictRemoteProctoringRooms(cc.getExamId());
|
||||
applyProcotringInstruction(cc.getExamId(), cc.getConnectionToken(), proctoringRoom.name);
|
||||
this.examSessionCacheService.evictClientConnection(cc.getConnectionToken());
|
||||
final String activeCollectingAllRoom = getActiveCollectingAllRoom(cc.getExamId());
|
||||
if (activeCollectingAllRoom != null) {
|
||||
applyProcotringInstruction(cc.getExamId(), cc.getConnectionToken(), activeCollectingAllRoom);
|
||||
} else {
|
||||
applyProcotringInstruction(cc.getExamId(), cc.getConnectionToken(), proctoringRoom.name);
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to process proctoring room update for client connection: {}", cc.getConnectionToken(), e);
|
||||
|
|
|
@ -21,7 +21,6 @@ import java.util.stream.Collectors;
|
|||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
|
@ -314,9 +313,15 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
|
||||
@Override
|
||||
public Result<ClientConnectionData> getConnectionData(final String connectionToken) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_ACTIVE_CLIENT_CONNECTION);
|
||||
return cache.get(connectionToken, ClientConnectionData.class);
|
||||
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
|
||||
.getActiveClientConnection(connectionToken);
|
||||
if (activeClientConnection == null) {
|
||||
throw new NoSuchElementException("Client Connection with token: " + connectionToken);
|
||||
}
|
||||
|
||||
return activeClientConnection;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -383,7 +383,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT,
|
||||
+ API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public ProctoringSettings getExamProctoring(
|
||||
|
@ -402,7 +402,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT,
|
||||
+ API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public Exam saveExamProctoring(
|
||||
|
|
|
@ -9,19 +9,14 @@
|
|||
package ch.ethz.seb.sebserver.webservice.weblayer.api;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -44,25 +39,16 @@ import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
|
|||
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException;
|
||||
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.session.ExamProcotringRoomService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService;
|
||||
|
@ -76,27 +62,21 @@ public class ExamMonitoringController {
|
|||
|
||||
private final SEBClientConnectionService sebClientConnectionService;
|
||||
private final ExamSessionService examSessionService;
|
||||
private final ExamAdminService examAdminService;
|
||||
private final SEBInstructionService sebInstructionService;
|
||||
private final AuthorizationService authorization;
|
||||
private final PaginationService paginationService;
|
||||
private final ExamProcotringRoomService examProcotringRoomService;
|
||||
|
||||
public ExamMonitoringController(
|
||||
final ExamAdminService examAdminService,
|
||||
final SEBClientConnectionService sebClientConnectionService,
|
||||
final SEBInstructionService sebInstructionService,
|
||||
final AuthorizationService authorization,
|
||||
final PaginationService paginationService,
|
||||
final ExamProcotringRoomService examProcotringRoomService) {
|
||||
final PaginationService paginationService) {
|
||||
|
||||
this.examAdminService = examAdminService;
|
||||
this.sebClientConnectionService = sebClientConnectionService;
|
||||
this.examSessionService = sebClientConnectionService.getExamSessionService();
|
||||
this.sebInstructionService = sebInstructionService;
|
||||
this.authorization = authorization;
|
||||
this.paginationService = paginationService;
|
||||
this.examProcotringRoomService = examProcotringRoomService;
|
||||
}
|
||||
|
||||
/** This is called by Spring to initialize the WebDataBinder and is used here to
|
||||
|
@ -289,387 +269,6 @@ public class ExamMonitoringController {
|
|||
|
||||
}
|
||||
|
||||
//***********************************************************************************************
|
||||
//**** Proctoring
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_ROOMS_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public Collection<RemoteProctoringRoom> getDefaultProcotringRoomsOfExam(@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) {
|
||||
|
||||
return this.examProcotringRoomService
|
||||
.getProctoringRooms(examId)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public SEBProctoringConnectionData getProctorRoomData(
|
||||
@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,
|
||||
@RequestParam(name = SEBProctoringConnectionData.ATTR_ROOM_NAME, required = true) final String roomName,
|
||||
@RequestParam(name = SEBProctoringConnectionData.ATTR_SUBJECT, required = false) final String subject) {
|
||||
|
||||
this.authorization.check(
|
||||
PrivilegeType.READ,
|
||||
EntityType.EXAM,
|
||||
institutionId);
|
||||
|
||||
this.authorization.checkRead(
|
||||
this.examSessionService.getExamDAO().byPK(examId).getOrThrow());
|
||||
|
||||
return this.examSessionService.getRunningExam(examId)
|
||||
.flatMap(this.authorization::checkRead)
|
||||
.flatMap(this.examAdminService::getExamProctoring)
|
||||
.flatMap(proc -> this.examAdminService
|
||||
.getExamProctoringService(proc.serverType)
|
||||
.flatMap(s -> s.createProctorPublicRoomConnection(
|
||||
proc,
|
||||
roomName,
|
||||
StringUtils.isNoneBlank(subject) ? subject : roomName)))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public Collection<ClientConnection> getProctorRoomConnectionData(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID,
|
||||
required = true) final String roomName) {
|
||||
|
||||
this.authorization.check(
|
||||
PrivilegeType.READ,
|
||||
EntityType.EXAM,
|
||||
institutionId);
|
||||
|
||||
this.authorization.checkRead(
|
||||
this.examSessionService.getExamDAO().byPK(examId).getOrThrow());
|
||||
|
||||
return this.examProcotringRoomService.getRoomConnections(examId, roomName)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_BROADCAST_ON_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public void sendBroadcastOn(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID,
|
||||
required = false) final String roomName,
|
||||
@RequestParam(
|
||||
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
required = false) final String connectionTokens,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
|
||||
required = false) final Boolean sendReceiveAudio,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
|
||||
required = false) final Boolean sendReceiveVideo,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT,
|
||||
required = false) final Boolean sendAllowChat) {
|
||||
|
||||
this.authorization.check(
|
||||
PrivilegeType.READ,
|
||||
EntityType.EXAM,
|
||||
institutionId);
|
||||
|
||||
this.authorization.checkRead(
|
||||
this.examSessionService.getExamDAO().byPK(examId).getOrThrow());
|
||||
|
||||
final Map<String, String> attributes = createProctorInstructionAttributes(
|
||||
sendReceiveAudio,
|
||||
sendReceiveVideo,
|
||||
sendAllowChat,
|
||||
Constants.TRUE_STRING);
|
||||
|
||||
sendProctoringInstructions(examId, roomName, connectionTokens, attributes);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_BROADCAST_OFF_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public void sendBroadcastOff(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID,
|
||||
required = true) final String roomName,
|
||||
@RequestParam(
|
||||
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
required = true) final String connectionTokens,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
|
||||
required = false) final Boolean sendReceiveAudio,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
|
||||
required = false) final Boolean sendReceiveVideo,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT,
|
||||
required = false) final Boolean sendAllowChat) {
|
||||
|
||||
this.authorization.check(
|
||||
PrivilegeType.READ,
|
||||
EntityType.EXAM,
|
||||
institutionId);
|
||||
|
||||
this.authorization.checkRead(
|
||||
this.examSessionService.getExamDAO().byPK(examId).getOrThrow());
|
||||
|
||||
final Map<String, String> attributes = createProctorInstructionAttributes(
|
||||
sendReceiveAudio,
|
||||
sendReceiveVideo,
|
||||
sendAllowChat,
|
||||
Constants.FALSE_STRING);
|
||||
|
||||
if (attributes.isEmpty()) {
|
||||
log.warn("Missing reconfigure instruction attributes. Skip sending empty instruction to SEB clients");
|
||||
return;
|
||||
}
|
||||
|
||||
sendProctoringInstructions(examId, roomName, connectionTokens, attributes);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_REJOIN_EXAM_ROOM_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public void sendRejoinExamCollectionRoomToClients(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
required = true) final String connectionTokens) {
|
||||
|
||||
this.authorization.check(
|
||||
PrivilegeType.READ,
|
||||
EntityType.EXAM,
|
||||
institutionId);
|
||||
|
||||
this.authorization.checkRead(
|
||||
this.examSessionService.getExamDAO().byPK(examId).getOrThrow());
|
||||
|
||||
final ProctoringSettings settings = this.examSessionService
|
||||
.getRunningExam(examId)
|
||||
.flatMap(this.authorization::checkRead)
|
||||
.flatMap(this.examAdminService::getExamProctoring)
|
||||
.getOrThrow();
|
||||
|
||||
final ExamProctoringService examProctoringService = this.examAdminService
|
||||
.getExamProctoringService(settings.serverType)
|
||||
.getOrThrow();
|
||||
|
||||
Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
|
||||
.stream()
|
||||
.forEach(connectionToken -> {
|
||||
final Result<Void> result = examProctoringService
|
||||
.getClientExamCollectionRoomConnectionData(
|
||||
settings,
|
||||
connectionToken)
|
||||
.flatMap(data -> this.sendJoinInstruction(examId, connectionTokens, data));
|
||||
if (result.hasError()) {
|
||||
// TODO log
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.PROCTORING_PATH_SEGMENT
|
||||
+ API.PROCTORING_JOIN_ROOM_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public SEBProctoringConnectionData sendJoinProctoringRoomToClients(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = SEBProctoringConnectionData.ATTR_ROOM_NAME,
|
||||
required = true) final String roomName,
|
||||
@RequestParam(
|
||||
name = SEBProctoringConnectionData.ATTR_SUBJECT,
|
||||
required = false) final String subject,
|
||||
@RequestParam(
|
||||
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
required = true) final String connectionTokens) {
|
||||
|
||||
this.authorization.check(
|
||||
PrivilegeType.READ,
|
||||
EntityType.EXAM,
|
||||
institutionId);
|
||||
|
||||
this.authorization.checkRead(
|
||||
this.examSessionService.getExamDAO().byPK(examId).getOrThrow());
|
||||
|
||||
final ProctoringSettings settings = this.examSessionService
|
||||
.getRunningExam(examId)
|
||||
.flatMap(this.authorization::checkRead)
|
||||
.flatMap(this.examAdminService::getExamProctoring)
|
||||
.getOrThrow();
|
||||
|
||||
final ExamProctoringService examProctoringService = this.examAdminService
|
||||
.getExamProctoringService(settings.serverType)
|
||||
.getOrThrow();
|
||||
|
||||
if (StringUtils.isNotBlank(connectionTokens)) {
|
||||
final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR);
|
||||
(single
|
||||
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
|
||||
: Arrays.asList(connectionTokens))
|
||||
.stream()
|
||||
.map(connectionToken -> {
|
||||
final SEBProctoringConnectionData data = (single)
|
||||
? examProctoringService
|
||||
.getClientRoomConnectionData(settings, connectionToken)
|
||||
.getOrThrow()
|
||||
: examProctoringService
|
||||
.getClientRoomConnectionData(
|
||||
settings,
|
||||
connectionToken,
|
||||
roomName,
|
||||
(StringUtils.isNotBlank(subject)) ? subject : roomName)
|
||||
.getOrThrow();
|
||||
sendJoinInstruction(examId, connectionToken, data)
|
||||
.onError(error -> log.error(
|
||||
"Failed to send proctoring leave instruction to client: {} ",
|
||||
connectionToken, error));
|
||||
return data;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return examProctoringService.createProctorPublicRoomConnection(
|
||||
settings,
|
||||
roomName,
|
||||
(StringUtils.isNotBlank(subject)) ? subject : roomName)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
private void sendProctoringInstructions(
|
||||
final Long examId,
|
||||
final String roomName,
|
||||
final String connectionTokens,
|
||||
final Map<String, String> attributes) {
|
||||
|
||||
if (attributes.isEmpty()) {
|
||||
log.warn("Missing reconfigure instruction attributes. Skip sending empty instruction to SEB clients");
|
||||
return;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(connectionTokens)) {
|
||||
final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR);
|
||||
(single
|
||||
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
|
||||
: Arrays.asList(connectionTokens))
|
||||
.stream()
|
||||
.forEach(connectionToken -> {
|
||||
this.sebInstructionService.registerInstruction(
|
||||
examId,
|
||||
InstructionType.SEB_RECONFIGURE_SETTINGS,
|
||||
attributes,
|
||||
connectionToken,
|
||||
true)
|
||||
.onError(error -> log.error(
|
||||
"Failed to register reconfiguring instruction for connection: {}",
|
||||
connectionToken,
|
||||
error));
|
||||
|
||||
});
|
||||
} else if (StringUtils.isNotBlank(roomName)) {
|
||||
this.examProcotringRoomService.getRoomConnections(examId, roomName)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.forEach(connection -> {
|
||||
this.sebInstructionService.registerInstruction(
|
||||
examId,
|
||||
InstructionType.SEB_RECONFIGURE_SETTINGS,
|
||||
attributes,
|
||||
connection.connectionToken,
|
||||
true)
|
||||
.onError(error -> log.error(
|
||||
"Failed to register reconfiguring instruction for connection: {}",
|
||||
connection.connectionToken,
|
||||
error));
|
||||
});
|
||||
} else {
|
||||
throw new RuntimeException("API attribute validation error: missing "
|
||||
+ Domain.REMOTE_PROCTORING_ROOM.ATTR_ID + " and/or" +
|
||||
API.EXAM_API_SEB_CONNECTION_TOKEN + " attribute");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> createProctorInstructionAttributes(
|
||||
final Boolean sendReceiveAudio,
|
||||
final Boolean sendReceiveVideo,
|
||||
final Boolean sendAllowChat,
|
||||
final String flagValue) {
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
if (BooleanUtils.isTrue(sendReceiveAudio)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
|
||||
flagValue);
|
||||
}
|
||||
if (BooleanUtils.isTrue(sendReceiveVideo)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
|
||||
flagValue);
|
||||
}
|
||||
if (BooleanUtils.isTrue(sendAllowChat)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT,
|
||||
flagValue);
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
//**** Proctoring
|
||||
//***********************************************************************************************
|
||||
|
||||
private boolean hasRunningExamPrivilege(final Long examId, final Long institution) {
|
||||
return hasRunningExamPrivilege(
|
||||
this.examSessionService.getRunningExam(examId).getOr(null),
|
||||
|
@ -685,53 +284,4 @@ public class ExamMonitoringController {
|
|||
return exam.institutionId.equals(institution) && exam.isOwner(userId);
|
||||
}
|
||||
|
||||
private Result<Void> sendJoinInstruction(
|
||||
final Long examId,
|
||||
final String connectionToken,
|
||||
final SEBProctoringConnectionData data) {
|
||||
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE,
|
||||
ProctoringSettings.ProctoringServerType.JITSI_MEET.name());
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD,
|
||||
ClientInstruction.ProctoringInstructionMethod.JOIN.name());
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL,
|
||||
data.serverURL);
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM,
|
||||
data.roomName);
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN,
|
||||
data.accessToken);
|
||||
return this.sebInstructionService.registerInstruction(
|
||||
examId,
|
||||
InstructionType.SEB_PROCTORING,
|
||||
attributes,
|
||||
connectionToken,
|
||||
true);
|
||||
}
|
||||
|
||||
// private Result<Void> sendLeaveInstruction(
|
||||
// final Long examId,
|
||||
// final String connectionToken,
|
||||
// final SEBProctoringConnectionData data) {
|
||||
//
|
||||
// return sendProctorInstruction(
|
||||
// examId,
|
||||
// connectionToken,
|
||||
// data,
|
||||
// ClientInstruction.ProctoringInstructionMethod.LEAVE.name());
|
||||
// }
|
||||
|
||||
// PRIVATE RESULT<VOID> SENDPROCTORINSTRUCTION(
|
||||
// FINAL LONG EXAMID,
|
||||
// FINAL STRING CONNECTIONTOKEN,
|
||||
// FINAL SEBPROCTORINGCONNECTIONDATA DATA,
|
||||
// FINAL STRING METHOD) {
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,587 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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.webservice.weblayer.api;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
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.ExamProcotringRoomService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService;
|
||||
|
||||
@WebServiceProfile
|
||||
@RestController
|
||||
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_PROCTORING_ENDPOINT)
|
||||
public class ExamProctoringController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExamProctoringController.class);
|
||||
|
||||
private final ExamProcotringRoomService examProcotringRoomService;
|
||||
private final ExamAdminService examAdminService;
|
||||
private final SEBInstructionService sebInstructionService;
|
||||
private final AuthorizationService authorization;
|
||||
private final ExamSessionService examSessionService;
|
||||
|
||||
public ExamProctoringController(
|
||||
final ExamProcotringRoomService examProcotringRoomService,
|
||||
final ExamAdminService examAdminService,
|
||||
final SEBInstructionService sebInstructionService,
|
||||
final AuthorizationService authorization,
|
||||
final ExamSessionService examSessionService) {
|
||||
|
||||
this.examProcotringRoomService = examProcotringRoomService;
|
||||
this.examAdminService = examAdminService;
|
||||
this.sebInstructionService = sebInstructionService;
|
||||
this.authorization = authorization;
|
||||
this.examSessionService = examSessionService;
|
||||
}
|
||||
|
||||
/** This is called by Spring to initialize the WebDataBinder and is used here to
|
||||
* initialize the default value binding for the institutionId request-parameter
|
||||
* that has the current users insitutionId as default.
|
||||
*
|
||||
* See also UserService.addUsersInstitutionDefaultPropertySupport */
|
||||
@InitBinder
|
||||
public void initBinder(final WebDataBinder binder) {
|
||||
this.authorization
|
||||
.getUserService()
|
||||
.addUsersInstitutionDefaultPropertySupport(binder);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_PROCTORING_ROOMS_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public Collection<RemoteProctoringRoom> getDefaultProcotringRoomsOfExam(
|
||||
@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) {
|
||||
|
||||
return this.examProcotringRoomService
|
||||
.getProctoringRooms(examId)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public SEBProctoringConnectionData getProctorRoomData(
|
||||
@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,
|
||||
@RequestParam(name = SEBProctoringConnectionData.ATTR_ROOM_NAME, required = true) final String roomName,
|
||||
@RequestParam(name = SEBProctoringConnectionData.ATTR_SUBJECT, required = false) final String subject) {
|
||||
|
||||
checkAccess(institutionId, examId);
|
||||
|
||||
return this.examSessionService.getRunningExam(examId)
|
||||
.flatMap(this.authorization::checkRead)
|
||||
.flatMap(this.examAdminService::getExamProctoring)
|
||||
.flatMap(proc -> this.examAdminService
|
||||
.getExamProctoringService(proc.serverType)
|
||||
.flatMap(s -> s.createProctorPublicRoomConnection(
|
||||
proc,
|
||||
roomName,
|
||||
StringUtils.isNoneBlank(subject) ? subject : roomName)))
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public Collection<ClientConnection> getProctorRoomConnectionData(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID,
|
||||
required = true) final String roomName) {
|
||||
|
||||
checkAccess(institutionId, examId);
|
||||
|
||||
return this.examProcotringRoomService.getRoomConnections(examId, roomName)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_PROCTORING_BROADCAST_ON_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public void sendBroadcastOn(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID,
|
||||
required = false) final String roomName,
|
||||
@RequestParam(
|
||||
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
required = false) final String connectionTokens,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
|
||||
required = false) final Boolean sendReceiveAudio,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
|
||||
required = false) final Boolean sendReceiveVideo,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT,
|
||||
required = false) final Boolean sendAllowChat) {
|
||||
|
||||
checkAccess(institutionId, examId);
|
||||
|
||||
final Map<String, String> attributes = createProctorInstructionAttributes(
|
||||
sendReceiveAudio,
|
||||
sendReceiveVideo,
|
||||
sendAllowChat,
|
||||
Constants.TRUE_STRING);
|
||||
|
||||
sendProctoringInstructions(examId, roomName, connectionTokens, attributes);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_PROCTORING_BROADCAST_OFF_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public void sendBroadcastOff(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID,
|
||||
required = true) final String roomName,
|
||||
@RequestParam(
|
||||
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
required = true) final String connectionTokens,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
|
||||
required = false) final Boolean sendReceiveAudio,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
|
||||
required = false) final Boolean sendReceiveVideo,
|
||||
@RequestParam(
|
||||
name = ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT,
|
||||
required = false) final Boolean sendAllowChat) {
|
||||
|
||||
checkAccess(institutionId, examId);
|
||||
|
||||
final Map<String, String> attributes = createProctorInstructionAttributes(
|
||||
sendReceiveAudio,
|
||||
sendReceiveVideo,
|
||||
sendAllowChat,
|
||||
Constants.FALSE_STRING);
|
||||
|
||||
if (attributes.isEmpty()) {
|
||||
log.warn("Missing reconfigure instruction attributes. Skip sending empty instruction to SEB clients");
|
||||
return;
|
||||
}
|
||||
|
||||
sendProctoringInstructions(examId, roomName, connectionTokens, attributes);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_PROCTORING_REJOIN_COLLECTING_ROOM_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public void sendRejoinExamCollectionRoomToClients(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
required = true) final String connectionTokens) {
|
||||
|
||||
checkAccess(institutionId, examId);
|
||||
|
||||
final ProctoringSettings settings = this.examSessionService
|
||||
.getRunningExam(examId)
|
||||
.flatMap(this.examAdminService::getExamProctoring)
|
||||
.getOrThrow();
|
||||
|
||||
final ExamProctoringService examProctoringService = this.examAdminService
|
||||
.getExamProctoringService(settings.serverType)
|
||||
.getOrThrow();
|
||||
|
||||
Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
|
||||
.stream()
|
||||
.forEach(connectionToken -> {
|
||||
examProctoringService
|
||||
.getClientExamCollectionRoomConnectionData(
|
||||
settings,
|
||||
connectionToken)
|
||||
.flatMap(data -> this.sendJoinInstruction(examId, connectionToken, data))
|
||||
.onError(error -> log.error("Failed to send rejoin for: {} cause: {}",
|
||||
connectionToken,
|
||||
error.getMessage()));
|
||||
});
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_PROCTORING_JOIN_ROOM_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public SEBProctoringConnectionData sendJoinProctoringRoomToClients(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = SEBProctoringConnectionData.ATTR_ROOM_NAME,
|
||||
required = true) final String roomName,
|
||||
@RequestParam(
|
||||
name = SEBProctoringConnectionData.ATTR_SUBJECT,
|
||||
required = false) final String subject,
|
||||
@RequestParam(
|
||||
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
|
||||
required = true) final String connectionTokens) {
|
||||
|
||||
checkAccess(institutionId, examId);
|
||||
|
||||
final ProctoringSettings settings = this.examSessionService
|
||||
.getRunningExam(examId)
|
||||
.flatMap(this.examAdminService::getExamProctoring)
|
||||
.getOrThrow();
|
||||
|
||||
final ExamProctoringService examProctoringService = this.examAdminService
|
||||
.getExamProctoringService(settings.serverType)
|
||||
.getOrThrow();
|
||||
|
||||
if (StringUtils.isNotBlank(connectionTokens)) {
|
||||
final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR);
|
||||
(single
|
||||
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
|
||||
: Arrays.asList(connectionTokens))
|
||||
.stream()
|
||||
.forEach(connectionToken -> {
|
||||
final SEBProctoringConnectionData data = (single)
|
||||
? examProctoringService
|
||||
.getClientRoomConnectionData(settings, connectionToken)
|
||||
.onError(error -> log.error(
|
||||
"Failed to get client room connection data for {} cause: {}",
|
||||
connectionToken,
|
||||
error.getMessage()))
|
||||
.get()
|
||||
: examProctoringService
|
||||
.getClientRoomConnectionData(
|
||||
settings,
|
||||
connectionToken,
|
||||
roomName,
|
||||
(StringUtils.isNotBlank(subject)) ? subject : roomName)
|
||||
.onError(error -> log.error(
|
||||
"Failed to get client room connection data for {} cause: {}",
|
||||
connectionToken,
|
||||
error.getMessage()))
|
||||
.get();
|
||||
if (data != null) {
|
||||
sendJoinInstruction(examId, connectionToken, data)
|
||||
.onError(error -> log.error(
|
||||
"Failed to send proctoring leave instruction to client: {} cause: {}",
|
||||
connectionToken,
|
||||
error.getMessage()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return examProctoringService.createProctorPublicRoomConnection(
|
||||
settings,
|
||||
roomName,
|
||||
(StringUtils.isNotBlank(subject)) ? subject : roomName)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_PROCTORING_JON_ALL_COLLECTING_ROOM,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public SEBProctoringConnectionData sendJoinAllCollectingRoomToClients(
|
||||
@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,
|
||||
@RequestParam(
|
||||
name = SEBProctoringConnectionData.ATTR_SUBJECT,
|
||||
required = false) final String subject) {
|
||||
|
||||
checkAccess(institutionId, examId);
|
||||
|
||||
final ProctoringSettings settings = this.examSessionService
|
||||
.getRunningExam(examId)
|
||||
.flatMap(this.examAdminService::getExamProctoring)
|
||||
.getOrThrow();
|
||||
|
||||
final ExamProctoringService examProctoringService = this.examAdminService
|
||||
.getExamProctoringService(settings.serverType)
|
||||
.getOrThrow();
|
||||
|
||||
// first create and register a room to collect all connection of the exam
|
||||
// As long as the room exists new connections will join this room immediately
|
||||
// after have been applied to the default collecting room
|
||||
final String roomName = this.examProcotringRoomService.createCollectAllRoom(examId)
|
||||
.onError(error -> this.examProcotringRoomService.disposeCollectAllRoom(examId))
|
||||
.getOrThrow();
|
||||
|
||||
// get all active connections for the exam and send the join instruction
|
||||
this.examSessionService.getConnectionData(
|
||||
examId,
|
||||
cData -> cData.clientConnection.status == ConnectionStatus.ACTIVE
|
||||
|| cData.clientConnection.status == ConnectionStatus.CLOSED)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.forEach(cc -> {
|
||||
final SEBProctoringConnectionData data = examProctoringService
|
||||
.getClientRoomConnectionData(
|
||||
settings,
|
||||
cc.clientConnection.connectionToken,
|
||||
roomName,
|
||||
(StringUtils.isNotBlank(subject)) ? subject : roomName)
|
||||
.onError(error -> log.error(
|
||||
"Failed to get client room connection data for {} cause: {}",
|
||||
cc.clientConnection.connectionToken,
|
||||
error.getMessage()))
|
||||
.get();
|
||||
if (data != null) {
|
||||
sendJoinInstruction(examId, cc.clientConnection.connectionToken, data)
|
||||
.onError(error -> log.error(
|
||||
"Failed to send proctoring leave instruction to client: {} ",
|
||||
cc.clientConnection.connectionToken, error));
|
||||
}
|
||||
});
|
||||
|
||||
return examProctoringService.createProctorPublicRoomConnection(
|
||||
settings,
|
||||
roomName,
|
||||
(StringUtils.isNotBlank(subject)) ? subject : roomName)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_PROCTORING_REJON_ALL_COLLECTING_ROOM,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public void sendReJoinAllCollectingRoomToClients(
|
||||
@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);
|
||||
|
||||
final ProctoringSettings settings = this.examSessionService
|
||||
.getRunningExam(examId)
|
||||
.flatMap(this.examAdminService::getExamProctoring)
|
||||
.getOrThrow();
|
||||
|
||||
final ExamProctoringService examProctoringService = this.examAdminService
|
||||
.getExamProctoringService(settings.serverType)
|
||||
.getOrThrow();
|
||||
|
||||
// first unregister the current room to collect all connection of the exam
|
||||
this.examProcotringRoomService.disposeCollectAllRoom(examId);
|
||||
|
||||
// get all active connections for the exam and send the join instruction
|
||||
this.examSessionService.getConnectionData(
|
||||
examId,
|
||||
cData -> cData.clientConnection.status == ConnectionStatus.ACTIVE
|
||||
|| cData.clientConnection.status == ConnectionStatus.CLOSED)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.forEach(cc -> {
|
||||
examProctoringService
|
||||
.getClientExamCollectionRoomConnectionData(
|
||||
settings,
|
||||
cc.clientConnection)
|
||||
.flatMap(data -> this.sendJoinInstruction(
|
||||
examId,
|
||||
cc.clientConnection.connectionToken,
|
||||
data))
|
||||
.onError(error -> log.error("Failed to send rejoin for: {} cause: {}",
|
||||
cc.clientConnection.connectionToken,
|
||||
error.getMessage()));
|
||||
});
|
||||
}
|
||||
|
||||
private void sendProctoringInstructions(
|
||||
final Long examId,
|
||||
final String roomName,
|
||||
final String connectionTokens,
|
||||
final Map<String, String> attributes) {
|
||||
|
||||
if (attributes.isEmpty()) {
|
||||
log.warn("Missing reconfigure instruction attributes. Skip sending empty instruction to SEB clients");
|
||||
return;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(connectionTokens)) {
|
||||
final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR);
|
||||
(single
|
||||
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
|
||||
: Arrays.asList(connectionTokens))
|
||||
.stream()
|
||||
.forEach(connectionToken -> {
|
||||
this.sebInstructionService.registerInstruction(
|
||||
examId,
|
||||
InstructionType.SEB_RECONFIGURE_SETTINGS,
|
||||
attributes,
|
||||
connectionToken,
|
||||
true)
|
||||
.onError(error -> log.error(
|
||||
"Failed to register reconfiguring instruction for connection: {}",
|
||||
connectionToken,
|
||||
error));
|
||||
|
||||
});
|
||||
} else if (StringUtils.isNotBlank(roomName)) {
|
||||
this.examProcotringRoomService.getRoomConnections(examId, roomName)
|
||||
.getOrThrow()
|
||||
.stream()
|
||||
.forEach(connection -> {
|
||||
this.sebInstructionService.registerInstruction(
|
||||
examId,
|
||||
InstructionType.SEB_RECONFIGURE_SETTINGS,
|
||||
attributes,
|
||||
connection.connectionToken,
|
||||
true)
|
||||
.onError(error -> log.error(
|
||||
"Failed to register reconfiguring instruction for connection: {}",
|
||||
connection.connectionToken,
|
||||
error));
|
||||
});
|
||||
} else {
|
||||
throw new RuntimeException("API attribute validation error: missing "
|
||||
+ Domain.REMOTE_PROCTORING_ROOM.ATTR_ID + " and/or" +
|
||||
API.EXAM_API_SEB_CONNECTION_TOKEN + " attribute");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> createProctorInstructionAttributes(
|
||||
final Boolean sendReceiveAudio,
|
||||
final Boolean sendReceiveVideo,
|
||||
final Boolean sendAllowChat,
|
||||
final String flagValue) {
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
if (BooleanUtils.isTrue(sendReceiveAudio)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
|
||||
flagValue);
|
||||
}
|
||||
if (BooleanUtils.isTrue(sendReceiveVideo)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
|
||||
flagValue);
|
||||
}
|
||||
if (BooleanUtils.isTrue(sendAllowChat)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT,
|
||||
flagValue);
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private Result<Void> sendJoinInstruction(
|
||||
final Long examId,
|
||||
final String connectionToken,
|
||||
final SEBProctoringConnectionData data) {
|
||||
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE,
|
||||
ProctoringSettings.ProctoringServerType.JITSI_MEET.name());
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD,
|
||||
ClientInstruction.ProctoringInstructionMethod.JOIN.name());
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL,
|
||||
data.serverURL);
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM,
|
||||
data.roomName);
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN,
|
||||
data.accessToken);
|
||||
return this.sebInstructionService.registerInstruction(
|
||||
examId,
|
||||
InstructionType.SEB_PROCTORING,
|
||||
attributes,
|
||||
connectionToken,
|
||||
true);
|
||||
}
|
||||
|
||||
private void checkAccess(final Long institutionId, final Long examId) {
|
||||
this.authorization.check(
|
||||
PrivilegeType.READ,
|
||||
EntityType.EXAM,
|
||||
institutionId);
|
||||
|
||||
this.authorization.checkRead(this.examSessionService
|
||||
.getExamDAO()
|
||||
.byPK(examId)
|
||||
.getOrThrow());
|
||||
}
|
||||
|
||||
}
|
|
@ -1449,7 +1449,9 @@ sebserver.monitoring.exam.action.list.view=Monitoring
|
|||
sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} )
|
||||
sebserver.exam.monitoring.action.category.filter=Filter
|
||||
sebserver.exam.overall.action.category.proctoring=Proctoring
|
||||
sebserver.monitoring.exam.action.proctoring.allRoom=View All
|
||||
|
||||
sebserver.monitoring.exam.proctoring.room.all.name=Exam Room
|
||||
sebserver.monitoring.exam.proctoring.action.close=Close Window
|
||||
sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast
|
||||
sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End Audio Broadcast
|
||||
|
|
Loading…
Reference in a new issue