Merge remote-tracking branch 'origin/dev-1.1.0' into development
Conflicts: src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java
This commit is contained in:
		
						commit
						6ff8b703c9
					
				
					 13 changed files with 402 additions and 194 deletions
				
			
		|  | @ -40,6 +40,9 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| @Order(7) | @Order(7) | ||||||
| public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements ErrorController { | public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements ErrorController { | ||||||
| 
 | 
 | ||||||
|  |     private static final String ERROR_PATH = "/sebserver/error"; | ||||||
|  |     private static final String CHECK_PATH = "/sebserver/check"; | ||||||
|  | 
 | ||||||
|     @Value("${sebserver.webservice.http.redirect.gui}") |     @Value("${sebserver.webservice.http.redirect.gui}") | ||||||
|     private String guiRedirect; |     private String guiRedirect; | ||||||
|     @Value("${sebserver.webservice.api.exam.endpoint.discovery}") |     @Value("${sebserver.webservice.api.exam.endpoint.discovery}") | ||||||
|  | @ -78,22 +81,27 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E | ||||||
|     public void configure(final WebSecurity web) { |     public void configure(final WebSecurity web) { | ||||||
|         web |         web | ||||||
|                 .ignoring() |                 .ignoring() | ||||||
|                 .antMatchers("/error") |                 .antMatchers(ERROR_PATH) | ||||||
|  |                 .antMatchers(CHECK_PATH) | ||||||
|                 .antMatchers(this.examAPIDiscoveryEndpoint) |                 .antMatchers(this.examAPIDiscoveryEndpoint) | ||||||
|                 .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**") |                 .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**") | ||||||
|                 .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**") |                 .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**") | ||||||
|                 .antMatchers(this.adminAPIEndpoint + API.REGISTER_ENDPOINT); |                 .antMatchers(this.adminAPIEndpoint + API.REGISTER_ENDPOINT); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @RequestMapping("/error") |     @RequestMapping(CHECK_PATH) | ||||||
|  |     public void check() throws IOException { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @RequestMapping(ERROR_PATH) | ||||||
|     public void handleError(final HttpServletResponse response) throws IOException { |     public void handleError(final HttpServletResponse response) throws IOException { | ||||||
|         //response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); |         response.getOutputStream().print(response.getStatus()); | ||||||
|         response.setHeader(HttpHeaders.LOCATION, this.guiRedirect); |         response.setHeader(HttpHeaders.LOCATION, this.guiRedirect); | ||||||
|         response.flushBuffer(); |         response.flushBuffer(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public String getErrorPath() { |     public String getErrorPath() { | ||||||
|         return "/error"; |         return ERROR_PATH; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -185,6 +185,7 @@ public final class API { | ||||||
|     public static final String EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections"; |     public static final String EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections"; | ||||||
|     public static final String EXAM_PROCTORING_ACTIVATE_TOWNHALL_ROOM = "activate-towhall-room"; |     public static final String EXAM_PROCTORING_ACTIVATE_TOWNHALL_ROOM = "activate-towhall-room"; | ||||||
|     public static final String EXAM_PROCTORING_TOWNHALL_ROOM_DATA = "towhall-room-data"; |     public static final String EXAM_PROCTORING_TOWNHALL_ROOM_DATA = "towhall-room-data"; | ||||||
|  |     public static final String EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE = "towhall-available"; | ||||||
| 
 | 
 | ||||||
|     public static final String EXAM_PROCTORING_ATTR_RECEIVE_AUDIO = "receive_audio"; |     public static final String EXAM_PROCTORING_ATTR_RECEIVE_AUDIO = "receive_audio"; | ||||||
|     public static final String EXAM_PROCTORING_ATTR_RECEIVE_VIDEO = "receive_video"; |     public static final String EXAM_PROCTORING_ATTR_RECEIVE_VIDEO = "receive_video"; | ||||||
|  |  | ||||||
|  | @ -116,13 +116,9 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati | ||||||
| 
 | 
 | ||||||
|         final String institutionalEndpoint = extractInstitutionalEndpoint(request); |         final String institutionalEndpoint = extractInstitutionalEndpoint(request); | ||||||
| 
 | 
 | ||||||
|         if (StringUtils.isNoneBlank(institutionalEndpoint) && log.isDebugEnabled()) { |         if (StringUtils.isNotBlank(institutionalEndpoint)) { | ||||||
|  |             if (log.isDebugEnabled()) { | ||||||
|                 log.debug("No default gui entrypoint requested: {}", institutionalEndpoint); |                 log.debug("No default gui entrypoint requested: {}", institutionalEndpoint); | ||||||
|         } else { |  | ||||||
|             request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, null); |  | ||||||
|             request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); |  | ||||||
|             forwardToEntryPoint(request, response, this.guiEntryPoint, false); |  | ||||||
|             return; |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
|  | @ -163,13 +159,13 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati | ||||||
|                 } |                 } | ||||||
|             } catch (final Exception e) { |             } catch (final Exception e) { | ||||||
|                 log.error("Failed to extract and set institutional endpoint request: ", e); |                 log.error("Failed to extract and set institutional endpoint request: ", e); | ||||||
| 
 |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, null); |         request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, null); | ||||||
|         request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); |         request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); | ||||||
|         response.setStatus(HttpStatus.UNAUTHORIZED.value()); |         response.setStatus(HttpStatus.UNAUTHORIZED.value()); | ||||||
|         forwardToEntryPoint(request, response, this.guiEntryPoint, true); |         forwardToEntryPoint(request, response, this.guiEntryPoint, institutionalEndpoint == null); | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -203,16 +199,25 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati | ||||||
| 
 | 
 | ||||||
|     public static String extractInstitutionalEndpoint(final HttpServletRequest request) { |     public static String extractInstitutionalEndpoint(final HttpServletRequest request) { | ||||||
|         final String requestURI = request.getRequestURI(); |         final String requestURI = request.getRequestURI(); | ||||||
|         if (StringUtils.isBlank(requestURI) || requestURI.equals(Constants.SLASH.toString())) { |         if (StringUtils.isBlank(requestURI)) { | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (requestURI.equals(Constants.SLASH.toString())) { | ||||||
|  |             return StringUtils.EMPTY; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|             if (log.isDebugEnabled()) { |             if (log.isDebugEnabled()) { | ||||||
|                 log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI); |                 log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|         try { |             final String[] split = StringUtils.split(requestURI, Constants.SLASH); | ||||||
|             return requestURI.substring(requestURI.lastIndexOf(Constants.SLASH) + 1); |             if (split.length > 1) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return split[0]; | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|             log.error("Failed to extract institutional URL suffix: {}", e.getMessage()); |             log.error("Failed to extract institutional URL suffix: {}", e.getMessage()); | ||||||
|             return null; |             return null; | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ import java.util.function.BooleanSupplier; | ||||||
| import java.util.function.Consumer; | import java.util.function.Consumer; | ||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
| 
 | 
 | ||||||
|  | import org.apache.commons.lang3.BooleanUtils; | ||||||
| import org.eclipse.rap.rwt.RWT; | import org.eclipse.rap.rwt.RWT; | ||||||
| import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; | import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; | ||||||
| import org.eclipse.swt.SWT; | import org.eclipse.swt.SWT; | ||||||
|  | @ -77,7 +78,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctorin | ||||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList; | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList; | ||||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; | ||||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; | ||||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetTownhallRoom; | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable; | ||||||
| import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; | import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; | ||||||
| import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; | import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; | ||||||
| import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; | import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; | ||||||
|  | @ -123,6 +124,7 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
| 
 | 
 | ||||||
|     private final ServerPushService serverPushService; |     private final ServerPushService serverPushService; | ||||||
|     private final PageService pageService; |     private final PageService pageService; | ||||||
|  |     private final RestService restService; | ||||||
|     private final ResourceService resourceService; |     private final ResourceService resourceService; | ||||||
|     private final AsyncRunner asyncRunner; |     private final AsyncRunner asyncRunner; | ||||||
|     private final InstructionProcessor instructionProcessor; |     private final InstructionProcessor instructionProcessor; | ||||||
|  | @ -147,6 +149,7 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
| 
 | 
 | ||||||
|         this.serverPushService = serverPushService; |         this.serverPushService = serverPushService; | ||||||
|         this.pageService = pageService; |         this.pageService = pageService; | ||||||
|  |         this.restService = pageService.getRestService(); | ||||||
|         this.resourceService = pageService.getResourceService(); |         this.resourceService = pageService.getResourceService(); | ||||||
|         this.asyncRunner = asyncRunner; |         this.asyncRunner = asyncRunner; | ||||||
|         this.instructionProcessor = instructionProcessor; |         this.instructionProcessor = instructionProcessor; | ||||||
|  | @ -214,13 +217,14 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
| 
 | 
 | ||||||
|         this.serverPushService.runServerPush( |         this.serverPushService.runServerPush( | ||||||
|                 new ServerPushContext( |                 new ServerPushContext( | ||||||
|                         content, Utils.truePredicate(), |                         content, | ||||||
|  |                         Utils.truePredicate(), | ||||||
|                         createServerPushUpdateErrorHandler(this.pageService, pageContext)), |                         createServerPushUpdateErrorHandler(this.pageService, pageContext)), | ||||||
|                 this.pollInterval, |                 this.pollInterval, | ||||||
|                 context -> clientTable.updateValues(), |                 context -> clientTable.updateValues(), | ||||||
|                 updateTableGUI(clientTable)); |                 updateTableGUI(clientTable)); | ||||||
| 
 | 
 | ||||||
|         final BooleanSupplier privilege = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER); |         final BooleanSupplier isExamSupporter = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER); | ||||||
| 
 | 
 | ||||||
|         actionBuilder |         actionBuilder | ||||||
| 
 | 
 | ||||||
|  | @ -242,20 +246,20 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
| 
 | 
 | ||||||
|                     return copyOfPageAction; |                     return copyOfPageAction; | ||||||
|                 }) |                 }) | ||||||
|                 .publishIf(privilege, false) |                 .publishIf(isExamSupporter, false) | ||||||
| 
 | 
 | ||||||
|                 .newAction(ActionDefinition.MONITOR_EXAM_QUIT_ALL) |                 .newAction(ActionDefinition.MONITOR_EXAM_QUIT_ALL) | ||||||
|                 .withEntityKey(entityKey) |                 .withEntityKey(entityKey) | ||||||
|                 .withConfirm(() -> CONFIRM_QUIT_ALL) |                 .withConfirm(() -> CONFIRM_QUIT_ALL) | ||||||
|                 .withExec(action -> this.quitSEBClients(action, clientTable, true)) |                 .withExec(action -> this.quitSEBClients(action, clientTable, true)) | ||||||
|                 .noEventPropagation() |                 .noEventPropagation() | ||||||
|                 .publishIf(privilege) |                 .publishIf(isExamSupporter) | ||||||
| 
 | 
 | ||||||
|                 .newAction(ActionDefinition.MONITORING_EXAM_SEARCH_CONNECTIONS) |                 .newAction(ActionDefinition.MONITORING_EXAM_SEARCH_CONNECTIONS) | ||||||
|                 .withEntityKey(entityKey) |                 .withEntityKey(entityKey) | ||||||
|                 .withExec(this::openSearchPopup) |                 .withExec(this::openSearchPopup) | ||||||
|                 .noEventPropagation() |                 .noEventPropagation() | ||||||
|                 .publishIf(privilege) |                 .publishIf(isExamSupporter) | ||||||
| 
 | 
 | ||||||
|                 .newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED) |                 .newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED) | ||||||
|                 .withEntityKey(entityKey) |                 .withEntityKey(entityKey) | ||||||
|  | @ -265,7 +269,7 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|                         action -> this.quitSEBClients(action, clientTable, false), |                         action -> this.quitSEBClients(action, clientTable, false), | ||||||
|                         EMPTY_ACTIVE_SELECTION_TEXT_KEY) |                         EMPTY_ACTIVE_SELECTION_TEXT_KEY) | ||||||
|                 .noEventPropagation() |                 .noEventPropagation() | ||||||
|                 .publishIf(privilege, false) |                 .publishIf(isExamSupporter, false) | ||||||
| 
 | 
 | ||||||
|                 .newAction(ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION) |                 .newAction(ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION) | ||||||
|                 .withEntityKey(entityKey) |                 .withEntityKey(entityKey) | ||||||
|  | @ -275,31 +279,111 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|                         action -> this.disableSEBClients(action, clientTable, false), |                         action -> this.disableSEBClients(action, clientTable, false), | ||||||
|                         EMPTY_SELECTION_TEXT_KEY) |                         EMPTY_SELECTION_TEXT_KEY) | ||||||
|                 .noEventPropagation() |                 .noEventPropagation() | ||||||
|                 .publishIf(privilege, false); |                 .publishIf(isExamSupporter, false); | ||||||
| 
 | 
 | ||||||
|         if (privilege.getAsBoolean()) { |         if (isExamSupporter.getAsBoolean()) { | ||||||
|  |             addFilterActions(actionBuilder, clientTable, isExamSupporter); | ||||||
|  |             addProctoringActions( | ||||||
|  |                     currentUser.getProctoringGUIService(), | ||||||
|  |                     pageContext, | ||||||
|  |                     content, | ||||||
|  |                     actionBuilder); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|             if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) { |     private void addProctoringActions( | ||||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) |             final ProctoringGUIService proctoringGUIService, | ||||||
|                         .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) |             final PageContext pageContext, | ||||||
|  |             final Composite parent, | ||||||
|  |             final PageActionBuilder actionBuilder) { | ||||||
|  | 
 | ||||||
|  |         final EntityKey entityKey = pageContext.getEntityKey(); | ||||||
|  |         final ProctoringServiceSettings proctoringSettings = this.restService | ||||||
|  |                 .getBuilder(GetProctoringSettings.class) | ||||||
|  |                 .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) | ||||||
|  |                 .call() | ||||||
|  |                 .getOr(null); | ||||||
|  | 
 | ||||||
|  |         if (proctoringSettings != null && proctoringSettings.enableProctoring) { | ||||||
|  | 
 | ||||||
|  |             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) | ||||||
|  |                     .withEntityKey(entityKey) | ||||||
|  |                     .withExec(action -> this.toggleTownhallRoom(proctoringGUIService, action)) | ||||||
|  |                     .noEventPropagation() | ||||||
|  |                     .publish(); | ||||||
|  | 
 | ||||||
|  |             if (isTownhallRoomActive(entityKey.modelId)) { | ||||||
|  |                 this.pageService.firePageEvent( | ||||||
|  |                         new ActionActivationEvent( | ||||||
|  |                                 true, | ||||||
|  |                                 new Tuple<>( | ||||||
|  |                                         ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||||
|  |                                         ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), | ||||||
|  |                         pageContext); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final Map<String, Pair<RemoteProctoringRoom, TreeItem>> availableRooms = new HashMap<>(); | ||||||
|  |             updateRoomActions( | ||||||
|  |                     pageContext, | ||||||
|  |                     availableRooms, | ||||||
|  |                     actionBuilder, | ||||||
|  |                     proctoringSettings, | ||||||
|  |                     proctoringGUIService); | ||||||
|  |             this.serverPushService.runServerPush( | ||||||
|  |                     new ServerPushContext( | ||||||
|  |                             parent, | ||||||
|  |                             Utils.truePredicate(), | ||||||
|  |                             createServerPushUpdateErrorHandler(this.pageService, pageContext)), | ||||||
|  |                     this.proctoringRoomUpdateInterval, | ||||||
|  |                     context -> updateRoomActions( | ||||||
|  |                             pageContext, | ||||||
|  |                             availableRooms, | ||||||
|  |                             actionBuilder, | ||||||
|  |                             proctoringSettings, | ||||||
|  |                             proctoringGUIService)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addFilterActions( | ||||||
|  |             final PageActionBuilder actionBuilder, | ||||||
|  |             final ClientConnectionTable clientTable, | ||||||
|  |             final BooleanSupplier isExamSupporter) { | ||||||
|  | 
 | ||||||
|  |         addClosedFilterAction(actionBuilder, clientTable); | ||||||
|  |         addRequestedFilterAction(actionBuilder, clientTable); | ||||||
|  |         addDisabledFilterAction(actionBuilder, clientTable); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addDisabledFilterAction( | ||||||
|  |             final PageActionBuilder actionBuilder, | ||||||
|  |             final ClientConnectionTable clientTable) { | ||||||
|  | 
 | ||||||
|  |         if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) { | ||||||
|  |             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) | ||||||
|  |                     .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||||
|                     .noEventPropagation() |                     .noEventPropagation() | ||||||
|                     .withSwitchAction( |                     .withSwitchAction( | ||||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) |                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) | ||||||
|                                         .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) |                                     .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||||
|                                     .noEventPropagation() |                                     .noEventPropagation() | ||||||
|                                     .create()) |                                     .create()) | ||||||
|                     .publish(); |                     .publish(); | ||||||
|         } else { |         } else { | ||||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) |             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) | ||||||
|                         .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) |                     .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||||
|                     .noEventPropagation() |                     .noEventPropagation() | ||||||
|                     .withSwitchAction( |                     .withSwitchAction( | ||||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) |                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) | ||||||
|                                         .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) |                                     .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) | ||||||
|                                     .noEventPropagation() |                                     .noEventPropagation() | ||||||
|                                     .create()) |                                     .create()) | ||||||
|                     .publish(); |                     .publish(); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addRequestedFilterAction( | ||||||
|  |             final PageActionBuilder actionBuilder, | ||||||
|  |             final ClientConnectionTable clientTable) { | ||||||
| 
 | 
 | ||||||
|         if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) { |         if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) { | ||||||
|             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) |             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) | ||||||
|  | @ -324,85 +408,42 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|                                     .create()) |                                     .create()) | ||||||
|                     .publish(); |                     .publish(); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|             if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) { |     private void addClosedFilterAction( | ||||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) |             final PageActionBuilder actionBuilder, | ||||||
|                         .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) |             final ClientConnectionTable clientTable) { | ||||||
|  | 
 | ||||||
|  |         if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) { | ||||||
|  |             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) | ||||||
|  |                     .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||||
|                     .noEventPropagation() |                     .noEventPropagation() | ||||||
|                     .withSwitchAction( |                     .withSwitchAction( | ||||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) |                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) | ||||||
|                                         .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) |                                     .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||||
|                                     .noEventPropagation() |                                     .noEventPropagation() | ||||||
|                                     .create()) |                                     .create()) | ||||||
|                     .publish(); |                     .publish(); | ||||||
|         } else { |         } else { | ||||||
|                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) |             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) | ||||||
|                         .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) |                     .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||||
|                     .noEventPropagation() |                     .noEventPropagation() | ||||||
|                     .withSwitchAction( |                     .withSwitchAction( | ||||||
|                                 actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) |                             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) | ||||||
|                                         .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) |                                     .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) | ||||||
|                                     .noEventPropagation() |                                     .noEventPropagation() | ||||||
|                                     .create()) |                                     .create()) | ||||||
|                     .publish(); |                     .publish(); | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         final ProctoringServiceSettings proctoringSettings = restService |  | ||||||
|                 .getBuilder(GetProctoringSettings.class) |  | ||||||
|                 .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) |  | ||||||
|                 .call() |  | ||||||
|                 .getOr(null); |  | ||||||
| 
 |  | ||||||
|         if (proctoringSettings != null && proctoringSettings.enableProctoring) { |  | ||||||
| 
 |  | ||||||
|             actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) |  | ||||||
|                     .withEntityKey(entityKey) |  | ||||||
|                     .withExec(this::toggleTownhallRoom) |  | ||||||
|                     .noEventPropagation() |  | ||||||
|                     .publish(); |  | ||||||
| 
 |  | ||||||
|             if (isTownhallRoomActive(entityKey.modelId)) { |  | ||||||
|                 this.pageService.firePageEvent( |  | ||||||
|                         new ActionActivationEvent( |  | ||||||
|                                 true, |  | ||||||
|                                 new Tuple<>( |  | ||||||
|                                         ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, |  | ||||||
|                                         ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), |  | ||||||
|                         pageContext); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             final Map<String, Pair<RemoteProctoringRoom, TreeItem>> availableRooms = new HashMap<>(); |  | ||||||
|             updateRoomActions( |  | ||||||
|                     entityKey, |  | ||||||
|                     pageContext, |  | ||||||
|                     availableRooms, |  | ||||||
|                     actionBuilder, |  | ||||||
|                     proctoringSettings); |  | ||||||
|             this.serverPushService.runServerPush( |  | ||||||
|                     new ServerPushContext( |  | ||||||
|                             content, |  | ||||||
|                             Utils.truePredicate(), |  | ||||||
|                             createServerPushUpdateErrorHandler(this.pageService, pageContext)), |  | ||||||
|                     this.proctoringRoomUpdateInterval, |  | ||||||
|                     context -> updateRoomActions( |  | ||||||
|                             entityKey, |  | ||||||
|                             pageContext, |  | ||||||
|                             availableRooms, |  | ||||||
|                             actionBuilder, |  | ||||||
|                             proctoringSettings)); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private boolean isTownhallRoomActive(final String examModelId) { |     private boolean isTownhallRoomActive(final String examModelId) { | ||||||
|         final RemoteProctoringRoom townhall = this.pageService.getRestService() |         return !BooleanUtils.toBoolean(this.pageService | ||||||
|                 .getBuilder(GetTownhallRoom.class) |                 .getRestService() | ||||||
|  |                 .getBuilder(IsTownhallRoomAvailable.class) | ||||||
|                 .withURIVariable(API.PARAM_MODEL_ID, examModelId) |                 .withURIVariable(API.PARAM_MODEL_ID, examModelId) | ||||||
|                 .call() |                 .call() | ||||||
|                 .getOr(null); |                 .getOr(Constants.FALSE_STRING)); | ||||||
| 
 |  | ||||||
|         return townhall != null && townhall.id != null; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private PageAction openSearchPopup(final PageAction action) { |     private PageAction openSearchPopup(final PageAction action) { | ||||||
|  | @ -410,9 +451,12 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|         return action; |         return action; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private PageAction toggleTownhallRoom(final PageAction action) { |     private PageAction toggleTownhallRoom( | ||||||
|  |             final ProctoringGUIService proctoringGUIService, | ||||||
|  |             final PageAction action) { | ||||||
|  | 
 | ||||||
|         if (isTownhallRoomActive(action.getEntityKey().modelId)) { |         if (isTownhallRoomActive(action.getEntityKey().modelId)) { | ||||||
|             closeTownhallRoom(action); |             closeTownhallRoom(proctoringGUIService, action); | ||||||
|             this.pageService.firePageEvent( |             this.pageService.firePageEvent( | ||||||
|                     new ActionActivationEvent( |                     new ActionActivationEvent( | ||||||
|                             true, |                             true, | ||||||
|  | @ -422,7 +466,7 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|                     action.pageContext()); |                     action.pageContext()); | ||||||
|             return action; |             return action; | ||||||
|         } else { |         } else { | ||||||
|             openTownhallRoom(action); |             openTownhallRoom(proctoringGUIService, action); | ||||||
|             this.pageService.firePageEvent( |             this.pageService.firePageEvent( | ||||||
|                     new ActionActivationEvent( |                     new ActionActivationEvent( | ||||||
|                             true, |                             true, | ||||||
|  | @ -434,14 +478,13 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private PageAction openTownhallRoom(final PageAction action) { |     private PageAction openTownhallRoom( | ||||||
|  |             final ProctoringGUIService proctoringGUIService, | ||||||
|  |             final PageAction action) { | ||||||
|  | 
 | ||||||
|         try { |         try { | ||||||
|             final EntityKey examId = action.getEntityKey(); |             final EntityKey examId = action.getEntityKey(); | ||||||
| 
 | 
 | ||||||
|             final ProctoringGUIService proctoringGUIService = this.pageService |  | ||||||
|                     .getCurrentUser() |  | ||||||
|                     .getProctoringGUIService(); |  | ||||||
| 
 |  | ||||||
|             final String windowName = getTownhallWindowName(examId.modelId); |             final String windowName = getTownhallWindowName(examId.modelId); | ||||||
|             if (!proctoringGUIService.hasWindow(windowName)) { |             if (!proctoringGUIService.hasWindow(windowName)) { | ||||||
|                 final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService |                 final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService | ||||||
|  | @ -475,11 +518,10 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|         return action; |         return action; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private String getTownhallWindowName(final String examId) { |     private PageAction closeTownhallRoom( | ||||||
|         return examId + "_townhall"; |             final ProctoringGUIService proctoringGUIService, | ||||||
|     } |             final PageAction action) { | ||||||
| 
 | 
 | ||||||
|     private PageAction closeTownhallRoom(final PageAction action) { |  | ||||||
|         final String examId = action.getEntityKey().modelId; |         final String examId = action.getEntityKey().modelId; | ||||||
|         try { |         try { | ||||||
| 
 | 
 | ||||||
|  | @ -494,8 +536,15 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|         return action; |         return action; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void updateTownhallButton(final EntityKey entityKey, final PageContext pageContext) { |     private void updateTownhallButton( | ||||||
|  |             final ProctoringGUIService proctoringGUIService, | ||||||
|  |             final PageContext pageContext) { | ||||||
|  |         final EntityKey entityKey = pageContext.getEntityKey(); | ||||||
|  | 
 | ||||||
|         if (isTownhallRoomActive(entityKey.modelId)) { |         if (isTownhallRoomActive(entityKey.modelId)) { | ||||||
|  |             final boolean townhallRoomFromThisUser = proctoringGUIService | ||||||
|  |                     .isTownhallOpenForUser(entityKey.modelId); | ||||||
|  |             if (townhallRoomFromThisUser) { | ||||||
|                 this.pageService.firePageEvent( |                 this.pageService.firePageEvent( | ||||||
|                         new ActionActivationEvent( |                         new ActionActivationEvent( | ||||||
|                                 true, |                                 true, | ||||||
|  | @ -503,25 +552,36 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|                                         ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, |                                         ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||||
|                                         ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), |                                         ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), | ||||||
|                         pageContext); |                         pageContext); | ||||||
|  |             } else { | ||||||
|  |                 this.pageService.firePageEvent( | ||||||
|  |                         new ActionActivationEvent( | ||||||
|  |                                 false, | ||||||
|  |                                 ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||||
|  |                                 ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), | ||||||
|  |                         pageContext); | ||||||
|  |             } | ||||||
|         } else { |         } else { | ||||||
|             this.pageService.firePageEvent( |             this.pageService.firePageEvent( | ||||||
|                     new ActionActivationEvent( |                     new ActionActivationEvent( | ||||||
|                             true, |                             true, | ||||||
|                             new Tuple<>( |                             new Tuple<>( | ||||||
|                                     ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, |                                     ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||||
|                                     ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)), |                                     ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM), | ||||||
|  |                             ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, | ||||||
|  |                             ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), | ||||||
|                     pageContext); |                     pageContext); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void updateRoomActions( |     private void updateRoomActions( | ||||||
|             final EntityKey entityKey, |  | ||||||
|             final PageContext pageContext, |             final PageContext pageContext, | ||||||
|             final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms, |             final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms, | ||||||
|             final PageActionBuilder actionBuilder, |             final PageActionBuilder actionBuilder, | ||||||
|             final ProctoringServiceSettings proctoringSettings) { |             final ProctoringServiceSettings proctoringSettings, | ||||||
|  |             final ProctoringGUIService proctoringGUIService) { | ||||||
| 
 | 
 | ||||||
|         updateTownhallButton(entityKey, pageContext); |         final EntityKey entityKey = pageContext.getEntityKey(); | ||||||
|  |         updateTownhallButton(proctoringGUIService, pageContext); | ||||||
|         final I18nSupport i18nSupport = this.pageService.getI18nSupport(); |         final I18nSupport i18nSupport = this.pageService.getI18nSupport(); | ||||||
|         this.pageService |         this.pageService | ||||||
|                 .getRestService() |                 .getRestService() | ||||||
|  | @ -564,7 +624,7 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|                         this.pageService.publishAction( |                         this.pageService.publishAction( | ||||||
|                                 action, |                                 action, | ||||||
|                                 _treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem))); |                                 _treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem))); | ||||||
|                         addRoomConnectionsPopupListener(entityKey, pageContext, rooms); |                         addRoomConnectionsPopupListener(pageContext, rooms); | ||||||
|                         processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext); |                         processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext); | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|  | @ -589,11 +649,11 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void addRoomConnectionsPopupListener( |     private void addRoomConnectionsPopupListener( | ||||||
|             final EntityKey entityKey, |  | ||||||
|             final PageContext pageContext, |             final PageContext pageContext, | ||||||
|             final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms) { |             final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms) { | ||||||
| 
 | 
 | ||||||
|         if (!rooms.isEmpty()) { |         if (!rooms.isEmpty()) { | ||||||
|  |             final EntityKey entityKey = pageContext.getEntityKey(); | ||||||
|             final TreeItem treeItem = rooms.values().iterator().next().b; |             final TreeItem treeItem = rooms.values().iterator().next().b; | ||||||
|             final Tree tree = treeItem.getParent(); |             final Tree tree = treeItem.getParent(); | ||||||
|             if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) { |             if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) { | ||||||
|  | @ -775,4 +835,8 @@ public class MonitoringRunningExam implements TemplateComposer { | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private String getTownhallWindowName(final String examId) { | ||||||
|  |         return examId + "_townhall"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -84,17 +84,18 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { | ||||||
|     public void compose(final PageContext pageContext) { |     public void compose(final PageContext pageContext) { | ||||||
| 
 | 
 | ||||||
|         final ProctoringWindowData proctoringWindowData = ProctoringGUIService.getCurrentProctoringWindowData(); |         final ProctoringWindowData proctoringWindowData = ProctoringGUIService.getCurrentProctoringWindowData(); | ||||||
| 
 |  | ||||||
|         final Composite parent = pageContext.getParent(); |         final Composite parent = pageContext.getParent(); | ||||||
| 
 |  | ||||||
|         final Composite content = new Composite(parent, SWT.NONE | SWT.NO_SCROLL); |         final Composite content = new Composite(parent, SWT.NONE | SWT.NO_SCROLL); | ||||||
|         final GridLayout gridLayout = new GridLayout(); |         final GridLayout gridLayout = new GridLayout(); | ||||||
|  |         final ProctoringGUIService proctoringGUIService = this.pageService | ||||||
|  |                 .getCurrentUser() | ||||||
|  |                 .getProctoringGUIService(); | ||||||
| 
 | 
 | ||||||
|         content.setLayout(gridLayout); |         content.setLayout(gridLayout); | ||||||
|         final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); |         final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); | ||||||
|         content.setLayoutData(headerCell); |         content.setLayoutData(headerCell); | ||||||
| 
 | 
 | ||||||
|         parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData)); |         parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData)); | ||||||
| 
 | 
 | ||||||
|         final String url = this.guiServiceInfo |         final String url = this.guiServiceInfo | ||||||
|                 .getExternalServerURIBuilder() |                 .getExternalServerURIBuilder() | ||||||
|  | @ -124,7 +125,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { | ||||||
| 
 | 
 | ||||||
|         final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY); |         final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY); | ||||||
|         closeAction.setLayoutData(new RowData(150, 30)); |         closeAction.setLayoutData(new RowData(150, 30)); | ||||||
|         closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringWindowData)); |         closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringGUIService, proctoringWindowData)); | ||||||
| 
 | 
 | ||||||
|         final BroadcastActionState broadcastActionState = new BroadcastActionState(); |         final BroadcastActionState broadcastActionState = new BroadcastActionState(); | ||||||
| 
 | 
 | ||||||
|  | @ -232,11 +233,15 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { | ||||||
|         sendReconfigurationAttributes(examId, roomName, state); |         sendReconfigurationAttributes(examId, roomName, state); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void closeRoom(final ProctoringWindowData proctoringWindowData) { |     private void closeRoom( | ||||||
|         this.pageService |             final ProctoringGUIService proctoringGUIService, | ||||||
|                 .getCurrentUser() |             final ProctoringWindowData proctoringWindowData) { | ||||||
|                 .getProctoringGUIService() | 
 | ||||||
|                 .closeRoomWindow(proctoringWindowData.windowName); |         try { | ||||||
|  |             proctoringGUIService.closeRoomWindow(proctoringWindowData.connectionData.roomName); | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.error("Failed to close proctoring window properly: ", e); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static final class BroadcastActionState { |     static final class BroadcastActionState { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,41 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2021 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 IsTownhallRoomAvailable extends RestCall<String> { | ||||||
|  | 
 | ||||||
|  |     public IsTownhallRoomAvailable() { | ||||||
|  |         super(new TypeKey<>( | ||||||
|  |                 CallType.GET_SINGLE, | ||||||
|  |                 EntityType.REMOTE_PROCTORING_ROOM, | ||||||
|  |                 new TypeReference<String>() { | ||||||
|  |                 }), | ||||||
|  |                 HttpMethod.GET, | ||||||
|  |                 MediaType.APPLICATION_FORM_URLENCODED, | ||||||
|  |                 API.EXAM_PROCTORING_ENDPOINT | ||||||
|  |                         + API.MODEL_ID_VAR_PATH_SEGMENT | ||||||
|  |                         + API.EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -50,6 +50,13 @@ public class ProctoringGUIService { | ||||||
|         this.openWindows.put(windowName, new RoomData(roomName, examId)); |         this.openWindows.put(windowName, new RoomData(roomName, examId)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public boolean isTownhallOpenForUser(final String examId) { | ||||||
|  |         return this.openWindows.values().stream() | ||||||
|  |                 .filter(room -> room.isTownhall && room.examId.equals(examId)) | ||||||
|  |                 .findFirst() | ||||||
|  |                 .isPresent(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public static ProctoringWindowData getCurrentProctoringWindowData() { |     public static ProctoringWindowData getCurrentProctoringWindowData() { | ||||||
|         return (ProctoringWindowData) RWT.getUISession() |         return (ProctoringWindowData) RWT.getUISession() | ||||||
|                 .getHttpSession() |                 .getHttpSession() | ||||||
|  | @ -105,7 +112,7 @@ public class ProctoringGUIService { | ||||||
|                 .withFormParam(ProctoringRoomConnection.ATTR_SUBJECT, subject) |                 .withFormParam(ProctoringRoomConnection.ATTR_SUBJECT, subject) | ||||||
|                 .call() |                 .call() | ||||||
|                 .map(connection -> { |                 .map(connection -> { | ||||||
|                     this.openWindows.put(windowName, new RoomData(connection.roomName, examId)); |                     this.openWindows.put(windowName, new RoomData(connection.roomName, examId, true)); | ||||||
|                     return connection; |                     return connection; | ||||||
|                 }); |                 }); | ||||||
|     } |     } | ||||||
|  | @ -152,10 +159,16 @@ public class ProctoringGUIService { | ||||||
|     private static final class RoomData { |     private static final class RoomData { | ||||||
|         final String roomName; |         final String roomName; | ||||||
|         final String examId; |         final String examId; | ||||||
|  |         final boolean isTownhall; | ||||||
| 
 | 
 | ||||||
|         public RoomData(final String roomName, final String examId) { |         public RoomData(final String roomName, final String examId) { | ||||||
|  |             this(roomName, examId, false); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public RoomData(final String roomName, final String examId, final boolean townhall) { | ||||||
|             this.roomName = roomName; |             this.roomName = roomName; | ||||||
|             this.examId = examId; |             this.examId = examId; | ||||||
|  |             this.isTownhall = townhall; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -59,6 +59,8 @@ public interface RemoteProctoringRoomDAO { | ||||||
|      * @return Result refer to the created room record or to an error when happened */ |      * @return Result refer to the created room record or to an error when happened */ | ||||||
|     Result<RemoteProctoringRoom> createTownhallRoom(Long examId, NewRoom room); |     Result<RemoteProctoringRoom> createTownhallRoom(Long examId, NewRoom room); | ||||||
| 
 | 
 | ||||||
|  |     boolean isTownhallRoomActive(Long examId); | ||||||
|  | 
 | ||||||
|     /** Get the town hall room record for a given exam if existing. |     /** Get the town hall room record for a given exam if existing. | ||||||
|      * |      * | ||||||
|      * @param examId the exam identifier |      * @param examId the exam identifier | ||||||
|  |  | ||||||
|  | @ -20,6 +20,8 @@ import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
| import org.apache.commons.lang3.BooleanUtils; | import org.apache.commons.lang3.BooleanUtils; | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.context.annotation.Lazy; | import org.springframework.context.annotation.Lazy; | ||||||
| import org.springframework.stereotype.Component; | import org.springframework.stereotype.Component; | ||||||
| import org.springframework.transaction.annotation.Transactional; | import org.springframework.transaction.annotation.Transactional; | ||||||
|  | @ -42,6 +44,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.New | ||||||
| @WebServiceProfile | @WebServiceProfile | ||||||
| public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { | public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { | ||||||
| 
 | 
 | ||||||
|  |     private static final Logger log = LoggerFactory.getLogger(RemoteProctoringRoomDAOImpl.class); | ||||||
|  | 
 | ||||||
|     private static final Object RESERVE_ROOM_LOCK = new Object(); |     private static final Object RESERVE_ROOM_LOCK = new Object(); | ||||||
| 
 | 
 | ||||||
|     private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper; |     private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper; | ||||||
|  | @ -129,15 +133,10 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { | ||||||
|             final NewRoom room) { |             final NewRoom room) { | ||||||
| 
 | 
 | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|             // check first if town-hall room is not already active |  | ||||||
|             final long active = this.remoteProctoringRoomRecordMapper.countByExample() |  | ||||||
|                     .where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId)) |  | ||||||
|                     .and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isNotEqualTo(0)) |  | ||||||
|                     .build() |  | ||||||
|                     .execute(); |  | ||||||
| 
 | 
 | ||||||
|             if (active > 0) { |             // Check first if town-hall room is not already active | ||||||
|                 throw new IllegalStateException("Townhall, for exam: " + examId + " already existis"); |             if (isTownhallRoomActive(examId)) { | ||||||
|  |                 throw new IllegalStateException("Townhall, for exam: " + examId + " already exists"); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             final RemoteProctoringRoomRecord townhallRoomRecord = new RemoteProctoringRoomRecord( |             final RemoteProctoringRoomRecord townhallRoomRecord = new RemoteProctoringRoomRecord( | ||||||
|  | @ -159,6 +158,24 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { | ||||||
|                 .onError(TransactionHandler::rollback); |                 .onError(TransactionHandler::rollback); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     @Transactional(readOnly = true) | ||||||
|  |     public boolean isTownhallRoomActive(final Long examId) { | ||||||
|  |         try { | ||||||
|  |             final long active = this.remoteProctoringRoomRecordMapper.countByExample() | ||||||
|  |                     .where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId)) | ||||||
|  |                     .and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isNotEqualTo(0)) | ||||||
|  |                     .build() | ||||||
|  |                     .execute(); | ||||||
|  |             return (active > 0); | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.error( | ||||||
|  |                     "Failed to verify town-hall room activity for exam: {}. Mark it as active to avoid double openings", | ||||||
|  |                     examId, e); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Override |     @Override | ||||||
|     @Transactional |     @Transactional | ||||||
|     public Result<EntityKey> deleteTownhallRoom(final Long examId) { |     public Result<EntityKey> deleteTownhallRoom(final Long examId) { | ||||||
|  |  | ||||||
|  | @ -58,6 +58,8 @@ public interface ExamProctoringRoomService { | ||||||
|      * @return Result refer to the given exam or to an error when happened */ |      * @return Result refer to the given exam or to an error when happened */ | ||||||
|     Result<Exam> disposeRoomsForExam(Exam exam); |     Result<Exam> disposeRoomsForExam(Exam exam); | ||||||
| 
 | 
 | ||||||
|  |     boolean isTownhallRoomActive(final Long examId); | ||||||
|  | 
 | ||||||
|     /** This creates a town-hall room for a specific exam. The exam must be active and running |     /** This creates a town-hall room for a specific exam. The exam must be active and running | ||||||
|      * and there must be no other town-hall room already be active. An unique room name will be |      * and there must be no other town-hall room already be active. An unique room name will be | ||||||
|      * created and returned. |      * created and returned. | ||||||
|  |  | ||||||
|  | @ -318,6 +318,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean isTownhallRoomActive(final Long examId) { | ||||||
|  |         return this.remoteProctoringRoomDAO.isTownhallRoomActive(examId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private void closeTownhall( |     private void closeTownhall( | ||||||
|             final Long examId, |             final Long examId, | ||||||
|             final ProctoringServiceSettings proctoringSettings, |             final ProctoringServiceSettings proctoringSettings, | ||||||
|  | @ -593,4 +598,5 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|                         true) |                         true) | ||||||
|                 .onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error)); |                 .onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error)); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ import javax.servlet.ServletOutputStream; | ||||||
| import javax.servlet.http.HttpServletRequest; | import javax.servlet.http.HttpServletRequest; | ||||||
| import javax.servlet.http.HttpServletResponse; | import javax.servlet.http.HttpServletResponse; | ||||||
| 
 | 
 | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.beans.factory.annotation.Qualifier; | import org.springframework.beans.factory.annotation.Qualifier; | ||||||
|  | @ -35,6 +36,7 @@ import org.springframework.web.bind.annotation.RequestParam; | ||||||
| import org.springframework.web.bind.annotation.ResponseStatus; | import org.springframework.web.bind.annotation.ResponseStatus; | ||||||
| import org.springframework.web.bind.annotation.RestController; | 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.API; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage; | import ch.ethz.seb.sebserver.gbl.api.APIMessage; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
|  | @ -101,7 +103,7 @@ public class ExamAPI_V1_Controller { | ||||||
| 
 | 
 | ||||||
|                     final POSTMapper mapper = new POSTMapper(formParams, request.getQueryString()); |                     final POSTMapper mapper = new POSTMapper(formParams, request.getQueryString()); | ||||||
| 
 | 
 | ||||||
|                     final String remoteAddr = request.getRemoteAddr(); |                     final String remoteAddr = this.getClientAddress(request); | ||||||
|                     final Long institutionId = (instIdRequestParam != null) |                     final Long institutionId = (instIdRequestParam != null) | ||||||
|                             ? instIdRequestParam |                             ? instIdRequestParam | ||||||
|                             : mapper.getLong(API.PARAM_INSTITUTION_ID); |                             : mapper.getLong(API.PARAM_INSTITUTION_ID); | ||||||
|  | @ -163,7 +165,7 @@ public class ExamAPI_V1_Controller { | ||||||
|         return CompletableFuture.runAsync( |         return CompletableFuture.runAsync( | ||||||
|                 () -> { |                 () -> { | ||||||
| 
 | 
 | ||||||
|                     final String remoteAddr = request.getRemoteAddr(); |                     final String remoteAddr = this.getClientAddress(request); | ||||||
|                     final Long institutionId = getInstitutionId(principal); |                     final Long institutionId = getInstitutionId(principal); | ||||||
| 
 | 
 | ||||||
|                     this.sebClientConnectionService.updateClientConnection( |                     this.sebClientConnectionService.updateClientConnection( | ||||||
|  | @ -193,7 +195,7 @@ public class ExamAPI_V1_Controller { | ||||||
|         return CompletableFuture.runAsync( |         return CompletableFuture.runAsync( | ||||||
|                 () -> { |                 () -> { | ||||||
| 
 | 
 | ||||||
|                     final String remoteAddr = request.getRemoteAddr(); |                     final String remoteAddr = this.getClientAddress(request); | ||||||
|                     final Long institutionId = getInstitutionId(principal); |                     final Long institutionId = getInstitutionId(principal); | ||||||
| 
 | 
 | ||||||
|                     this.sebClientConnectionService.establishClientConnection( |                     this.sebClientConnectionService.establishClientConnection( | ||||||
|  | @ -220,7 +222,7 @@ public class ExamAPI_V1_Controller { | ||||||
|         return CompletableFuture.runAsync( |         return CompletableFuture.runAsync( | ||||||
|                 () -> { |                 () -> { | ||||||
| 
 | 
 | ||||||
|                     final String remoteAddr = request.getRemoteAddr(); |                     final String remoteAddr = this.getClientAddress(request); | ||||||
|                     final Long institutionId = getInstitutionId(principal); |                     final Long institutionId = getInstitutionId(principal); | ||||||
| 
 | 
 | ||||||
|                     if (log.isDebugEnabled()) { |                     if (log.isDebugEnabled()) { | ||||||
|  | @ -406,4 +408,23 @@ public class ExamAPI_V1_Controller { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private String getClientAddress(final HttpServletRequest request) { | ||||||
|  |         try { | ||||||
|  |             final String ipAddress = request.getHeader("X-FORWARDED-FOR"); | ||||||
|  | 
 | ||||||
|  |             if (ipAddress == null) { | ||||||
|  |                 return request.getRemoteAddr(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (ipAddress.contains(",")) { | ||||||
|  |                 return StringUtils.split(ipAddress, Constants.COMMA)[0]; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return ipAddress; | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.warn("Failed to verify client IP address: {}", e.getMessage()); | ||||||
|  |             return request.getHeader("X-FORWARDED-FOR"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -234,6 +234,22 @@ public class ExamProctoringController { | ||||||
|                 .onError(error -> log.error("Failed to close remote proctoring break out room {}", roomName, error)); |                 .onError(error -> log.error("Failed to close remote proctoring break out room {}", roomName, error)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @RequestMapping( | ||||||
|  |             path = API.MODEL_ID_VAR_PATH_SEGMENT | ||||||
|  |                     + API.EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE, | ||||||
|  |             method = RequestMethod.GET, | ||||||
|  |             produces = MediaType.APPLICATION_JSON_VALUE) | ||||||
|  |     public String isTownhallRoomAvialbale( | ||||||
|  |             @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) { | ||||||
|  | 
 | ||||||
|  |         checkExamReadAccess(institutionId); | ||||||
|  |         return String.valueOf(!this.examProcotringRoomService.isTownhallRoomActive(examId)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @RequestMapping( |     @RequestMapping( | ||||||
|             path = API.MODEL_ID_VAR_PATH_SEGMENT |             path = API.MODEL_ID_VAR_PATH_SEGMENT | ||||||
|                     + API.EXAM_PROCTORING_TOWNHALL_ROOM_DATA, |                     + API.EXAM_PROCTORING_TOWNHALL_ROOM_DATA, | ||||||
|  | @ -246,7 +262,7 @@ public class ExamProctoringController { | ||||||
|                     defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, |                     defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, | ||||||
|             @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { |             @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { | ||||||
| 
 | 
 | ||||||
|         checkAccess(institutionId, examId); |         checkExamReadAccess(institutionId); | ||||||
|         return this.examProcotringRoomService |         return this.examProcotringRoomService | ||||||
|                 .getTownhallRoomData(examId) |                 .getTownhallRoomData(examId) | ||||||
|                 .getOrElse(() -> RemoteProctoringRoom.NULL_ROOM); |                 .getOrElse(() -> RemoteProctoringRoom.NULL_ROOM); | ||||||
|  | @ -273,6 +289,13 @@ public class ExamProctoringController { | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private void checkExamReadAccess(final Long institutionId) { | ||||||
|  |         this.authorization.check( | ||||||
|  |                 PrivilegeType.READ, | ||||||
|  |                 EntityType.EXAM, | ||||||
|  |                 institutionId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private void checkAccess(final Long institutionId, final Long examId) { |     private void checkAccess(final Long institutionId, final Long examId) { | ||||||
|         this.authorization.check( |         this.authorization.check( | ||||||
|                 PrivilegeType.READ, |                 PrivilegeType.READ, | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti