fixed proctor room action update
This commit is contained in:
parent
5b9b336886
commit
55cfc07a9d
4 changed files with 140 additions and 154 deletions
|
@ -77,7 +77,7 @@ public class MonitoringClientConnection implements TemplateComposer {
|
|||
|
||||
// @formatter:off
|
||||
private static final String OPEN_SINGEL_ROOM_SCRIPT =
|
||||
"var existingWin = window.open('', '%s', 'height=420,width=620,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes,dialog=yes');\n" +
|
||||
"var existingWin = window.open('', '%s', 'height=420,width=640,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes,dialog=yes');\n" +
|
||||
"if(existingWin.location.href === 'about:blank'){\n" +
|
||||
" existingWin.location.href = '%s%s';\n" +
|
||||
" existingWin.focus();\n" +
|
||||
|
|
|
@ -49,6 +49,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
|||
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Pair;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Tuple;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.gui.GuiServiceInfo;
|
||||
|
@ -114,6 +115,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
private final InstructionProcessor instructionProcessor;
|
||||
private final GuiServiceInfo guiServiceInfo;
|
||||
private final long pollInterval;
|
||||
private final long proctoringRoomUpdateInterval;
|
||||
private final String remoteProctoringEndpoint;
|
||||
private final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup;
|
||||
|
||||
|
@ -124,7 +126,8 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
final GuiServiceInfo guiServiceInfo,
|
||||
final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup,
|
||||
@Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval,
|
||||
@Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint) {
|
||||
@Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint,
|
||||
@Value("${sebserver.gui.remote.proctoring.rooms.update.poll-interval:5000}") final long proctoringRoomUpdateInterval) {
|
||||
|
||||
this.serverPushService = serverPushService;
|
||||
this.pageService = pageService;
|
||||
|
@ -134,6 +137,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
this.pollInterval = pollInterval;
|
||||
this.remoteProctoringEndpoint = remoteProctoringEndpoint;
|
||||
this.proctorRoomConnectionsPopup = proctorRoomConnectionsPopup;
|
||||
this.proctoringRoomUpdateInterval = proctoringRoomUpdateInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -325,7 +329,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
.getOr(null);
|
||||
|
||||
if (proctoringSettings != null && proctoringSettings.enableProctoring) {
|
||||
final Map<RemoteProctoringRoom, TreeItem> availableRooms = new HashMap<>();
|
||||
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> availableRooms = new HashMap<>();
|
||||
updateRoomActions(
|
||||
entityKey,
|
||||
pageContext,
|
||||
|
@ -334,7 +338,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
proctoringSettings);
|
||||
this.serverPushService.runServerPush(
|
||||
new ServerPushContext(content, Utils.truePredicate()),
|
||||
5000,
|
||||
this.proctoringRoomUpdateInterval,
|
||||
context -> updateRoomActions(
|
||||
entityKey,
|
||||
pageContext,
|
||||
|
@ -347,7 +351,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
private void updateRoomActions(
|
||||
final EntityKey entityKey,
|
||||
final PageContext pageContext,
|
||||
final Map<RemoteProctoringRoom, TreeItem> rooms,
|
||||
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms,
|
||||
final PageActionBuilder actionBuilder,
|
||||
final ProctoringSettings proctoringSettings) {
|
||||
|
||||
|
@ -358,9 +362,10 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
.getOrThrow()
|
||||
.stream()
|
||||
.forEach(room -> {
|
||||
if (rooms.containsKey(room)) {
|
||||
if (rooms.containsKey(room.name)) {
|
||||
// update action
|
||||
final TreeItem treeItem = rooms.get(room);
|
||||
final TreeItem treeItem = rooms.get(room.name).b;
|
||||
rooms.put(room.name, new Pair<>(room, treeItem));
|
||||
treeItem.setText(i18nSupport.getText(new LocTextKey(
|
||||
ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM.title.name,
|
||||
room.subject,
|
||||
|
@ -372,7 +377,13 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
final PageAction action =
|
||||
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(a -> showExamProctoringRoom(proctoringSettings, room, rooms, a))
|
||||
.withExec(_action -> {
|
||||
final int actualRoomSize = getActualRoomSize(room, rooms);
|
||||
if (actualRoomSize <= 0) {
|
||||
return _action;
|
||||
}
|
||||
return showExamProctoringRoom(proctoringSettings, room, _action);
|
||||
})
|
||||
.withNameAttributes(
|
||||
room.subject,
|
||||
room.roomSize,
|
||||
|
@ -380,15 +391,11 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
.noEventPropagation()
|
||||
.create();
|
||||
|
||||
this.pageService.publishAction(action, treeItem -> rooms.put(room, treeItem));
|
||||
this.pageService.publishAction(
|
||||
action,
|
||||
_treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem)));
|
||||
addRoomConnectionsPopupListener(entityKey, pageContext, rooms);
|
||||
rooms.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().equals(room))
|
||||
.findFirst()
|
||||
.ifPresent(entry -> processProctorRoomActionActivation(
|
||||
entry.getValue(),
|
||||
room,
|
||||
pageContext));
|
||||
processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -414,10 +421,10 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
private void addRoomConnectionsPopupListener(
|
||||
final EntityKey entityKey,
|
||||
final PageContext pageContext,
|
||||
final Map<RemoteProctoringRoom, TreeItem> rooms) {
|
||||
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms) {
|
||||
|
||||
if (!rooms.isEmpty()) {
|
||||
final TreeItem treeItem = rooms.values().iterator().next();
|
||||
final TreeItem treeItem = rooms.values().iterator().next().b;
|
||||
final Tree tree = treeItem.getParent();
|
||||
if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) {
|
||||
tree.addListener(SWT.Selection, event -> {
|
||||
|
@ -426,16 +433,17 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
if (event.button == 3) {
|
||||
rooms.entrySet()
|
||||
.stream()
|
||||
.filter(e -> e.getValue().equals(item))
|
||||
.filter(e -> e.getValue().b.equals(item))
|
||||
.findFirst()
|
||||
.ifPresent(e -> {
|
||||
if (e.getKey().roomSize > 0) {
|
||||
final RemoteProctoringRoom room = e.getValue().a;
|
||||
if (room.roomSize > 0) {
|
||||
final PageContext pc = pageContext.copy()
|
||||
.clearAttributes()
|
||||
.withEntityKey(new EntityKey(e.getKey().getName(),
|
||||
.withEntityKey(new EntityKey(room.name,
|
||||
EntityType.REMOTE_PROCTORING_ROOM))
|
||||
.withParentEntityKey(entityKey);
|
||||
this.proctorRoomConnectionsPopup.show(pc, e.getKey().getSubject());
|
||||
this.proctorRoomConnectionsPopup.show(pc, room.subject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -445,25 +453,18 @@ public class MonitoringRunningExam implements TemplateComposer {
|
|||
}
|
||||
}
|
||||
|
||||
private int getActualRoomSize(final RemoteProctoringRoom room, final Map<RemoteProctoringRoom, TreeItem> rooms) {
|
||||
return rooms.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().equals(room))
|
||||
.findFirst()
|
||||
.map(entry -> entry.getKey().roomSize)
|
||||
.orElseGet(() -> 1);
|
||||
private int getActualRoomSize(
|
||||
final RemoteProctoringRoom room,
|
||||
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms) {
|
||||
|
||||
return rooms.get(room.name).a.roomSize;
|
||||
}
|
||||
|
||||
private PageAction showExamProctoringRoom(
|
||||
final ProctoringSettings proctoringSettings,
|
||||
final RemoteProctoringRoom room,
|
||||
final Map<RemoteProctoringRoom, TreeItem> rooms,
|
||||
final PageAction action) {
|
||||
|
||||
final int actualRoomSize = getActualRoomSize(room, rooms);
|
||||
if (actualRoomSize <= 0) {
|
||||
return action;
|
||||
}
|
||||
|
||||
final SEBProctoringConnectionData proctoringConnectionData = this.pageService
|
||||
.getRestService()
|
||||
.getBuilder(GetProctorRoomConnectionData.class)
|
||||
|
|
|
@ -17,6 +17,8 @@ import org.eclipse.swt.layout.RowData;
|
|||
import org.eclipse.swt.layout.RowLayout;
|
||||
import org.eclipse.swt.widgets.Button;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
@ -41,6 +43,8 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
|||
@GuiProfile
|
||||
public class JitsiMeetProctoringView implements RemoteProctoringView {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JitsiMeetProctoringView.class);
|
||||
|
||||
private static final LocTextKey CLOSE_WINDOW_TEXT_KEY =
|
||||
new LocTextKey("sebserver.monitoring.exam.proctoring.action.close");
|
||||
private static final LocTextKey BROADCAST_AUDIO_ON_TEXT_KEY =
|
||||
|
@ -92,8 +96,17 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
|
|||
.getProctoringGUIService()
|
||||
.closeRoom(proctoringWindowData.connectionData.roomName));
|
||||
|
||||
final String url = this.guiServiceInfo.getExternalServerURIBuilder().toUriString()
|
||||
+ this.remoteProctoringEndpoint + this.remoteProctoringViewServletEndpoint + "/";
|
||||
final String url = this.guiServiceInfo
|
||||
.getExternalServerURIBuilder()
|
||||
.toUriString()
|
||||
+ this.remoteProctoringEndpoint
|
||||
+ this.remoteProctoringViewServletEndpoint
|
||||
+ Constants.SLASH;
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Open proctoring Servlet in IFrame with URL: {}", url);
|
||||
}
|
||||
|
||||
final Browser browser = new Browser(content, SWT.NONE | SWT.NO_SCROLL);
|
||||
browser.setLayout(new GridLayout());
|
||||
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
|
||||
|
@ -274,7 +287,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
|
|||
return ProctoringServerType.JITSI_MEET;
|
||||
}
|
||||
|
||||
private static class BroadcastActionState {
|
||||
private static final class BroadcastActionState {
|
||||
public static final String KEY_NAME = "BroadcastActionState";
|
||||
boolean audio = false;
|
||||
boolean video = false;
|
||||
|
|
|
@ -410,68 +410,13 @@ public class ExamMonitoringController {
|
|||
this.authorization.checkRead(
|
||||
this.examSessionService.getExamDAO().byPK(examId).getOrThrow());
|
||||
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
if (BooleanUtils.isTrue(sendReceiveAudio)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
|
||||
Constants.TRUE_STRING);
|
||||
}
|
||||
if (BooleanUtils.isTrue(sendReceiveVideo)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
|
||||
Constants.TRUE_STRING);
|
||||
}
|
||||
if (BooleanUtils.isTrue(sendAllowChat)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT,
|
||||
Constants.TRUE_STRING);
|
||||
}
|
||||
final Map<String, String> attributes = createProctorInstructionAttributes(
|
||||
sendReceiveAudio,
|
||||
sendReceiveVideo,
|
||||
sendAllowChat,
|
||||
Constants.TRUE_STRING);
|
||||
|
||||
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");
|
||||
}
|
||||
sendProctoringInstructions(examId, roomName, connectionTokens, attributes);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
|
@ -510,68 +455,18 @@ public class ExamMonitoringController {
|
|||
this.authorization.checkRead(
|
||||
this.examSessionService.getExamDAO().byPK(examId).getOrThrow());
|
||||
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
if (BooleanUtils.isTrue(sendReceiveAudio)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
|
||||
Constants.FALSE_STRING);
|
||||
}
|
||||
if (BooleanUtils.isTrue(sendReceiveVideo)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
|
||||
Constants.FALSE_STRING);
|
||||
}
|
||||
if (BooleanUtils.isTrue(sendAllowChat)) {
|
||||
attributes.put(
|
||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_ALLOW_CHAT,
|
||||
Constants.FALSE_STRING);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
sendProctoringInstructions(examId, roomName, connectionTokens, attributes);
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
|
@ -695,6 +590,83 @@ public class ExamMonitoringController {
|
|||
.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
|
||||
//***********************************************************************************************
|
||||
|
||||
|
|
Loading…
Reference in a new issue