From 7cfc327b87a9b5c251c0a48c7ba54a9dd8d11691 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 14 Nov 2024 09:36:55 +0100 Subject: [PATCH] SEBSP-116 fixes and better group handling (TODOs for performance improvements) --- .../ch/ethz/seb/sebserver/gbl/Constants.java | 3 + .../model/exam/ProctoringServiceSettings.java | 2 + .../model/exam/ScreenProctoringSettings.java | 1 + .../ProctoringGroupMonitoringData.java | 20 +++ .../AlphabeticalNameRangeMatcher.java | 7 +- .../monitoring/MonitoringFullPageData.java | 39 +++--- .../content/exam/ClientGroupTemplateForm.java | 41 +++--- .../sebserver/gui/content/exam/ExamForm.java | 2 + .../exam/ScreenProctoringSettingsPopup.java | 36 ++++-- .../gui/content/monitoring/FinishedExam.java | 4 +- .../monitoring/MonitoringRunningExam.java | 26 ++-- .../gui/service/ResourceService.java | 51 +++++--- .../gui/service/session/MonitoringFilter.java | 12 +- .../MonitoringProctoringService.java | 38 ++---- .../proctoring/ProctoringGUIService.java | 9 +- .../servicelayer/dao/ClientConnectionDAO.java | 3 + .../dao/impl/ClientConnectionDAOImpl.java | 19 +++ .../dao/impl/ProctoringSettingsDAOImpl.java | 10 +- .../impl/ScreenProctoringGroupDAOImpl.java | 1 - .../exam/ExamTemplateService.java | 2 +- .../exam/impl/ExamAdminServiceImpl.java | 28 ++-- .../exam/impl/ExamTemplateServiceImpl.java | 96 +++++++++++--- .../exam/impl/ProctoringAdminServiceImpl.java | 9 +- .../session/ScreenProctoringService.java | 13 +- .../session/impl/ExamSessionCacheService.java | 49 ++++--- .../session/impl/proctoring/SPS_API.java | 5 +- .../ScreenProctoringAPIBinding.java | 75 +++++++---- .../ScreenProctoringServiceImpl.java | 116 ++++++++++++++--- .../api/ExamMonitoringController.java | 25 +--- src/main/resources/messages.properties | 8 +- .../AlphabeticalNameRangeMatcherTest.java | 93 ++++++++++++++ .../integration/UseCasesIntegrationTest.java | 3 + .../admin/ExamProctoringRoomServiceTest.java | 121 ------------------ 33 files changed, 580 insertions(+), 387 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ProctoringGroupMonitoringData.java create mode 100644 src/test/java/ch/ethz/seb/sebserver/gbl/monitoring/AlphabeticalNameRangeMatcherTest.java delete mode 100644 src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java index e5d9b9012..32e45960b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java @@ -28,6 +28,9 @@ /** Global Constants used in SEB Server web-service as well as in web-gui component */ public final class Constants { + + public static final String UNICODE_LOWEST = Character.toString(0x00); + public static final String UNICODE_HIGHEST = Character.toString(0x10FFFF); public static final String FILE_EXT_CSV = ".csv"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java index ba16cb67e..7855def0d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java @@ -14,6 +14,7 @@ import java.util.Objects; import java.util.stream.Collectors; +import ch.ethz.seb.sebserver.gbl.util.Result; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.hibernate.validator.constraints.URL; @@ -32,6 +33,7 @@ @ValidProctoringSettings public class ProctoringServiceSettings implements Entity { + public enum ProctoringServerType { JITSI_MEET, ZOOM diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ScreenProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ScreenProctoringSettings.java index 6a8441343..cfaaaecf2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ScreenProctoringSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ScreenProctoringSettings.java @@ -233,4 +233,5 @@ public String toString() { ", bundled=" + bundled + '}'; } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ProctoringGroupMonitoringData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ProctoringGroupMonitoringData.java new file mode 100644 index 000000000..cc512a524 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ProctoringGroupMonitoringData.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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.gbl.model.session; + +import ch.ethz.seb.sebserver.gbl.model.Domain; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record ProctoringGroupMonitoringData( + @JsonProperty(Domain.SCREEN_PROCTORING_GROUP.ATTR_UUID) String uuid, + @JsonProperty(Domain.SCREEN_PROCTORING_GROUP.ATTR_NAME) String name, + @JsonProperty(Domain.SCREEN_PROCTORING_GROUP.ATTR_SIZE) Integer size) { +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/AlphabeticalNameRangeMatcher.java b/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/AlphabeticalNameRangeMatcher.java index 18d76a94c..e003cda0b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/AlphabeticalNameRangeMatcher.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/AlphabeticalNameRangeMatcher.java @@ -8,6 +8,7 @@ package ch.ethz.seb.sebserver.gbl.monitoring; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup; import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroupData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; @@ -33,7 +34,11 @@ public boolean isInGroup(final ClientConnection clientConnection, final ClientGr final String start = group.nameRangeStartLetter != null ? group.nameRangeStartLetter.substring(0, 1) : "A"; final String end = group.nameRangeStartLetter != null ? group.nameRangeEndLetter.substring(0, 1) : "Z"; + return isInRange(name, start, end); + } + + public boolean isInRange(final String name, final String start, final String end) { return name.compareToIgnoreCase(start) >= 0 && - name.compareToIgnoreCase(end) <= 0; + name.compareToIgnoreCase(end + Constants.UNICODE_HIGHEST) <= 0; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringFullPageData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringFullPageData.java index a60e29e0a..245f628d5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringFullPageData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringFullPageData.java @@ -10,6 +10,7 @@ import java.util.Collection; +import ch.ethz.seb.sebserver.gbl.model.session.ProctoringGroupMonitoringData; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -28,20 +29,20 @@ public class MonitoringFullPageData { public final Long examId; @JsonProperty(ATTR_CONNECTIONS_DATA) public final MonitoringSEBConnectionData monitoringConnectionData; - @JsonProperty(ATTR_PROCTORING_DATA) - public final Collection proctoringData; +// @JsonProperty(ATTR_PROCTORING_DATA) +// public final Collection proctoringData; @JsonProperty(ATTR_SCREEN_PROCTORING_DATA) - final Collection screenProctoringData; + final Collection screenProctoringData; public MonitoringFullPageData( @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId, @JsonProperty(ATTR_CONNECTIONS_DATA) final MonitoringSEBConnectionData monitoringConnectionData, - @JsonProperty(ATTR_PROCTORING_DATA) final Collection proctoringData, - @JsonProperty(ATTR_SCREEN_PROCTORING_DATA) final Collection screenProctoringData) { + // @JsonProperty(ATTR_PROCTORING_DATA) final Collection proctoringData, + @JsonProperty(ATTR_SCREEN_PROCTORING_DATA) final Collection screenProctoringData) { this.examId = examId; this.monitoringConnectionData = monitoringConnectionData; - this.proctoringData = proctoringData; + // this.proctoringData = proctoringData; this.screenProctoringData = screenProctoringData; } @@ -53,11 +54,11 @@ public MonitoringSEBConnectionData getMonitoringConnectionData() { return this.monitoringConnectionData; } - public Collection getProctoringData() { - return this.proctoringData; - } +// public Collection getProctoringData() { +// return this.proctoringData; +// } - public Collection getScreenProctoringData() { + public Collection getScreenProctoringData() { return this.screenProctoringData; } @@ -88,17 +89,13 @@ public boolean equals(final Object obj) { @Override public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append("MonitoringFullPageData [examId="); - builder.append(this.examId); - builder.append(", monitoringConnectionData="); - builder.append(this.monitoringConnectionData); - builder.append(", proctoringData="); - builder.append(this.proctoringData); - builder.append(", screenProctoringData="); - builder.append(this.screenProctoringData); - builder.append("]"); - return builder.toString(); + return "MonitoringFullPageData [examId=" + + this.examId + + ", monitoringConnectionData=" + + this.monitoringConnectionData + + ", screenProctoringData=" + + this.screenProctoringData + + "]"; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ClientGroupTemplateForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ClientGroupTemplateForm.java index bd974e431..30408b19d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ClientGroupTemplateForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ClientGroupTemplateForm.java @@ -52,8 +52,8 @@ public class ClientGroupTemplateForm implements TemplateComposer { new LocTextKey("sebserver.exam.clientgroup.form.type"); private static final LocTextKey FORM_NAME_TEXT_KEY = new LocTextKey("sebserver.exam.clientgroup.form.name"); - private static final LocTextKey FORM_EXAM_TEXT_KEY = - new LocTextKey("sebserver.exam.clientgroup.form.exam"); + private static final LocTextKey FORM_EXAM_TEMPLATE_TEXT_KEY = + new LocTextKey("sebserver.exam.clientgroup.form.exam-template"); private static final LocTextKey FORM_DESC_TEXT_KEY = new LocTextKey("sebserver.exam.clientgroup.form.description"); private static final LocTextKey FORM_IP_START_KEY = @@ -164,10 +164,24 @@ private void buildFormAccordingToSelection( final PageContext pageContext, final boolean init) { + + final String name = init + ? clientGroupTemplate.getName() + : formHandleAnchor.formHandle.getForm().getFieldValue(Domain.CLIENT_GROUP.ATTR_NAME); + final String color = init + ? clientGroupTemplate.getColor() + : formHandleAnchor.formHandle.getForm().getFieldValue(Domain.CLIENT_GROUP.ATTR_COLOR); + if (!init) { PageService.clearComposite(formHandleAnchor.formContext.getParent()); } + final ClientGroupData.ClientGroupType type = selection != null ? ClientGroupData.ClientGroupType.valueOf(selection) : null; + final String typeDescription = (type != null) + ? Utils.formatLineBreaks( + this.i18nSupport.getText(CLIENT_GROUP_TYPE_DESC_PREFIX + type.name())) + : Constants.EMPTY_NOTE; + final RestService restService = this.resourceService.getRestService(); final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); final EntityKey entityKey = pageContext.getEntityKey(); @@ -175,15 +189,7 @@ private void buildFormAccordingToSelection( final boolean isNew = entityKey == null; final boolean isReadonly = pageContext.isReadonly(); - final ClientGroupData.ClientGroupType type = selection != null - ? ClientGroupData.ClientGroupType.valueOf(selection) - : null; - final String typeDescription = (type != null) - ? Utils.formatLineBreaks( - this.i18nSupport.getText(CLIENT_GROUP_TYPE_DESC_PREFIX + clientGroupTemplate.type.name())) - : Constants.EMPTY_NOTE; - - final FormHandle formHandle = this.pageService.formBuilder(formHandleAnchor.formContext) + formHandleAnchor.formHandle = this.pageService.formBuilder(formHandleAnchor.formContext) .readonly(isReadonly) .putStaticValueIf(() -> !isNew, Domain.CLIENT_GROUP.ATTR_ID, @@ -196,26 +202,27 @@ private void buildFormAccordingToSelection( parentEntityKey.getModelId()) .addField(FormBuilder.text( - Domain.EXAM_TEMPLATE.ATTR_NAME, - FORM_EXAM_TEXT_KEY, + "templateName", + FORM_EXAM_TEMPLATE_TEXT_KEY, examTemplate.name) .readonly(true)) + .addField(FormBuilder.text( Domain.CLIENT_GROUP.ATTR_NAME, FORM_NAME_TEXT_KEY, - clientGroupTemplate.name) + name) .mandatory(!isReadonly)) .addField(FormBuilder.colorSelection( Domain.CLIENT_GROUP.ATTR_COLOR, FORM_COLOR_TEXT_KEY, - clientGroupTemplate.color) + color) .withEmptyCellSeparation(false)) .addField(FormBuilder.singleSelection( Domain.CLIENT_GROUP.ATTR_TYPE, FORM_TYPE_TEXT_KEY, - (clientGroupTemplate.type != null) ? clientGroupTemplate.type.name() : null, + type != null ? type.name() : null, this.resourceService::clientGroupTypeResources) .withSelectionListener(form -> buildFormAccordingToSelection( form.getFieldValue(Domain.CLIENT_GROUP.ATTR_TYPE), @@ -272,6 +279,8 @@ private void buildFormAccordingToSelection( .buildFor((isNew) ? restService.getRestCall(NewClientGroupTemplate.class) : restService.getRestCall(SaveClientGroupTemplate.class)); + + formHandleAnchor.formContext.getParent().layout(); } static final class FormHandleAnchor { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java index 445d5f68e..7d478a2af 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java @@ -203,6 +203,8 @@ public void compose(final PageContext pageContext) { final boolean modifyGrant = entityGrantCheck.m(); final boolean writeGrant = entityGrantCheck.w(); final boolean editable = modifyGrant && Exam.ACTIVE_STATES.contains(exam.getStatus()); + + final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean( exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); final boolean sebRestrictionAvailable = readonly && hasSEBRestrictionAPI(exam); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java index 7d88a9c69..2cfad3c34 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java @@ -12,9 +12,9 @@ import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; @@ -30,7 +30,6 @@ 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.model.Entity; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy; import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; @@ -94,6 +93,9 @@ public class ScreenProctoringSettingsPopup { new LocTextKey("sebserver.exam.sps.form.saveSettings.error"); private final static LocTextKey SAVE_TEXT_KEY = new LocTextKey("sebserver.exam.sps.form.saveSettings"); + + private final static LocTextKey ACTIVE_SEB_CLIENT_MSG = + new LocTextKey("sebserver.exam.sps.form.active-seb-clients"); Function settingsFunction(final PageService pageService, final boolean modifyGrant) { return action -> { @@ -215,15 +217,23 @@ private boolean doSaveSettings( action.pageContext()); return true; } else { - Exception error = saveRequest.getError(); + final Exception error = saveRequest.getError(); boolean onlyFieldErrors = false; if (error instanceof RestCallError) { - onlyFieldErrors = ((RestCallError) error) + final List noneFieldErrors = ((RestCallError) error) .getAPIMessages() .stream() .filter(message -> !APIMessage.ErrorMessage.FIELD_VALIDATION.isOf(message)) - .toList() - .isEmpty(); + .toList(); + + if (!noneFieldErrors.isEmpty()) { + if (APIMessage.ErrorMessage.CLIENT_CONNECTION_INTEGRITY_VIOLATION.isOf(noneFieldErrors.get(0))) { + pageContext.publishInfo(ACTIVE_SEB_CLIENT_MSG); + return false; + } + } + + onlyFieldErrors = noneFieldErrors.isEmpty(); } if (onlyFieldErrors) { @@ -238,7 +248,6 @@ private boolean doSaveSettings( } } } - return false; } @@ -260,6 +269,7 @@ private ScreenProctoringPropertiesForm( public Supplier> compose(final Composite parent) { final RestService restService = this.pageService.getRestService(); final EntityKey entityKey = this.pageContext.getEntityKey(); + final I18nSupport i18nSupport = this.pageService.getI18nSupport(); final Composite content = this.pageService .getWidgetFactory() @@ -277,6 +287,7 @@ public Supplier> compose(final Composite parent) { .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .call() .getOrThrow(); + final Composite formRoot = this.pageService.getWidgetFactory().voidComposite(content); final FormHandleAnchor formHandleAnchor = new FormHandleAnchor(); @@ -285,6 +296,7 @@ public Supplier> compose(final Composite parent) { .clearEntityKeys(); buildFormForCollectingStrategy( + entityKey, settings.collectingStrategy.name(), formHandleAnchor, settings, @@ -294,13 +306,18 @@ public Supplier> compose(final Composite parent) { } private void buildFormForCollectingStrategy( + final EntityKey entityKey, final String selection, final FormHandleAnchor formHandleAnchor, final ScreenProctoringSettings settings, final boolean init ) { + Boolean enableScreenProctoring = settings.enableScreenProctoring; + if (!init) { + enableScreenProctoring = BooleanUtils.toBoolean(formHandleAnchor.formHandle.getForm().getFieldValue( + ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING)); PageService.clearComposite(formHandleAnchor.formContext.getParent()); } @@ -327,7 +344,7 @@ private void buildFormForCollectingStrategy( .addField(FormBuilder.checkbox( ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING, FORM_ENABLE, - String.valueOf(settings.enableScreenProctoring))) + String.valueOf(enableScreenProctoring))) .addField(FormBuilder.text( ScreenProctoringSettings.ATTR_SPS_SERVICE_URL, @@ -376,6 +393,7 @@ private void buildFormForCollectingStrategy( selection, () -> this.pageService.getResourceService().getCollectingStrategySelection()) .withSelectionListener(f -> buildFormForCollectingStrategy( + entityKey, f.getFieldValue(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY), formHandleAnchor, settings, @@ -392,7 +410,7 @@ private void buildFormForCollectingStrategy( ScreenProctoringSettings.ATT_SEB_GROUPS_SELECTION, FORM_SEB_CLIENT_GROUPS, settings.sebGroupsSelection, - () -> this.pageService.getResourceService().getSEBGroupSelection(String.valueOf(settings.examId)))) + () -> this.pageService.getResourceService().getSEBGroupSelection(entityKey))) .addFieldIf( () -> CollectingStrategy.APPLY_SEB_GROUPS.name().equals(selection), () -> FormBuilder.text( diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/FinishedExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/FinishedExam.java index ce0423dc2..ada74fe40 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/FinishedExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/FinishedExam.java @@ -192,7 +192,7 @@ public void compose(final PageContext pageContext) { pageContext, ActionDefinition.VIEW_FINISHED_EXAM_CLIENT_CONNECTION)); - indicators.stream().forEach(indicator -> { + indicators.forEach(indicator -> { if (indicator.type == IndicatorType.LAST_PING || indicator.type == IndicatorType.NONE) { return; } @@ -244,7 +244,7 @@ public void compose(final PageContext pageContext) { .withEntityKey(exam.getEntityKey()) .withExec(_action -> monitoringProctoringService.openScreenProctoringTab( screenProctoringSettings, - group, + group.uuid, _action)) .withNameAttributes(group.name, group.size) .noEventPropagation() diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java index 8c21550be..784419b2e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java @@ -22,6 +22,7 @@ import java.util.function.Function; import ch.ethz.seb.sebserver.gbl.model.exam.AllowedSEBVersion; +import ch.ethz.seb.sebserver.gbl.model.session.ProctoringGroupMonitoringData; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.text.StringEscapeUtils; @@ -49,7 +50,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionIssueStatus; -import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; @@ -73,7 +73,6 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.clientgroup.GetClientGroups; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.indicator.GetIndicators; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetScreenProctoringGroups; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; @@ -378,17 +377,7 @@ private FullPageMonitoringGUIUpdate createProctoringActions( proctoringGUIService.clearActionState(); final EntityKey entityKey = pageContext.getEntityKey(); - final Collection collectingRooms = (proctoringEnabled) - ? this.pageService - .getRestService() - .getBuilder(GetCollectingRooms.class) - .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) - .call() - .onError(error -> log.error("Failed to get collecting room data:", error)) - .getOr(Collections.emptyList()) - : Collections.emptyList(); - - final Collection screenProctoringGroups = (screenProctoringEnabled) + final Collection screenProctoringGroups = ((screenProctoringEnabled) ? this.pageService .getRestService() .getBuilder(GetScreenProctoringGroups.class) @@ -396,11 +385,15 @@ private FullPageMonitoringGUIUpdate createProctoringActions( .call() .onError(error -> log.error("\"Failed to get screen proctoring group data:", error)) .getOr(Collections.emptyList()) - : Collections.emptyList(); + : Collections.emptyList()); + final List spMonitoringData = screenProctoringGroups + .stream() + .map(g -> new ProctoringGroupMonitoringData(g.uuid, g.name, g.size)) + .toList(); + this.monitoringProctoringService.updateCollectingRoomActions( - collectingRooms, - screenProctoringGroups, + spMonitoringData, pageContext, proctoringSettings, proctoringGUIService, @@ -408,7 +401,6 @@ private FullPageMonitoringGUIUpdate createProctoringActions( return monitoringStatus -> this.monitoringProctoringService .updateCollectingRoomActions( - monitoringStatus.proctoringData(), monitoringStatus.screenProctoringData(), pageContext, proctoringSettings, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java index 0a2dca707..aa2ea9651 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java @@ -14,9 +14,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.*; import ch.ethz.seb.sebserver.gbl.model.user.*; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.clientgroup.GetClientGroups; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplate; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTimeZone; import org.slf4j.Logger; @@ -961,21 +963,40 @@ public String collectingStrategyName(final ScreenProctoringSettings settings) { .getText(ResourceService.COLLECTING_STRATEGY_PREFIX + settings.collectingStrategy.name()); } - public List> getSEBGroupSelection(final String examModelId) { - return this.restService.getBuilder(GetClientGroups.class) - .withQueryParam(Indicator.FILTER_ATTR_EXAM_ID, examModelId) - .call() - .onError(error -> log.error("Failed to get SEB client groups for exam: {}, cause {}",examModelId, error.getMessage())) - .getOr(Collections.emptyList()) - .stream() - .map( group -> new Tuple3<>( - group.getModelId(), - group.name, - Utils.formatLineBreaks(this.i18nSupport.getText( - "sebserver.exam.proctoring.collecting.strategy.clientgroup.tooltip", - group.name)))) - .sorted(RESOURCE_COMPARATOR) - .collect(Collectors.toList()); + public List> getSEBGroupSelection(final EntityKey entityKey) { + if (entityKey.entityType == EntityType.EXAM) { + return this.restService.getBuilder(GetClientGroups.class) + .withQueryParam(Indicator.FILTER_ATTR_EXAM_ID, entityKey.modelId) + .call() + .onError(error -> log.error("Failed to get SEB client groups for exam: {}, cause {}", entityKey.modelId, error.getMessage())) + .getOr(Collections.emptyList()) + .stream() + .map(group -> new Tuple3<>( + group.getModelId(), + group.name, + Utils.formatLineBreaks(this.i18nSupport.getText( + "sebserver.exam.proctoring.collecting.strategy.clientgroup.tooltip", + group.name)))) + .sorted(RESOURCE_COMPARATOR) + .collect(Collectors.toList()); + } else { + return restService + .getBuilder(GetExamTemplate.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .map(t -> t.clientGroupTemplates + .stream() + .map( group -> (Tuple) new Tuple3<>( + group.getModelId(), + group.name, + Utils.formatLineBreaks(i18nSupport.getText( + "sebserver.exam.proctoring.collecting.strategy.clientgroup.tooltip", + group.name)))) + .toList() + ) + .onError(error -> log.error("Failed to get SEB client groups for exam template: {}, cause {}", entityKey.modelId, error.getMessage())) + .getOr(Collections.emptyList()); + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java index 540e671c2..9f44a6d47 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java @@ -16,8 +16,8 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionIssueStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringData; +import ch.ethz.seb.sebserver.gbl.model.session.ProctoringGroupMonitoringData; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; -import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData; @@ -108,15 +108,11 @@ default int getNumOfConnections(final ConnectionIssueStatus connectionIssueStatu } default Collection proctoringData() { - final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData(); - if (monitoringFullPageData != null) { - return monitoringFullPageData.proctoringData; - } else { - return null; - } + // not used anymore + return null; } - default Collection screenProctoringData() { + default Collection screenProctoringData() { final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData(); if (monitoringFullPageData != null) { return monitoringFullPageData.getScreenProctoringData(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java index ffc2006ce..d415af99c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java @@ -12,6 +12,7 @@ import java.util.Collection; import java.util.Map; +import ch.ethz.seb.sebserver.gbl.model.session.ProctoringGroupMonitoringData; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.BooleanUtils; @@ -46,7 +47,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; -import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.util.Cryptor; import ch.ethz.seb.sebserver.gbl.util.Tuple; @@ -163,20 +163,12 @@ public PageAction toggleTownhallRoom( } public void updateCollectingRoomActions( - final Collection collectingRooms, - final Collection screenProctoringGroups, + final Collection screenProctoringGroups, final PageContext pageContext, final ProctoringServiceSettings proctoringSettings, final ProctoringGUIService proctoringGUIService, final ScreenProctoringSettings screenProctoringSettings) { - collectingRooms - .forEach(room -> updateProctoringAction( - pageContext, - proctoringSettings, - proctoringGUIService, - room)); - if (BooleanUtils.isTrue(proctoringSettings.enableProctoring) && proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) { @@ -197,31 +189,31 @@ private void updateScreenProctoringAction( final PageContext pageContext, final ScreenProctoringSettings settings, final ProctoringGUIService proctoringGUIService, - final ScreenProctoringGroup group) { + final ProctoringGroupMonitoringData group) { final PageActionBuilder actionBuilder = this.pageService .pageActionBuilder(pageContext.clearEntityKeys()); final EntityKey entityKey = pageContext.getEntityKey(); final I18nSupport i18nSupport = this.pageService.getI18nSupport(); - final TreeItem screeProctoringGroupAction = proctoringGUIService.getScreeProctoringGroupAction(group); + final TreeItem screeProctoringGroupAction = proctoringGUIService.getScreeProctoringGroupAction(group.uuid()); if (screeProctoringGroupAction != null) { // update action screeProctoringGroupAction.setText(i18nSupport.getText(new LocTextKey( ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP.title.name, - group.name, - group.size))); + group.name(), + group.size()))); } else { // create action this.pageService.publishAction( actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP) .withEntityKey(entityKey) - .withExec(_action -> openScreenProctoringTab(settings, group, _action)) - .withNameAttributes(group.name, group.size) + .withExec(_action -> openScreenProctoringTab(settings, group.uuid(), _action)) + .withNameAttributes(group.name(), group.size()) .noEventPropagation() .create(), - _treeItem -> proctoringGUIService.registerScreeProctoringGroupAction(group, _treeItem)); + _treeItem -> proctoringGUIService.registerScreeProctoringGroupAction(group.uuid(), _treeItem)); } } @@ -307,7 +299,7 @@ private void showCollectingRoomPopup( public PageAction openScreenProctoringTab( final ScreenProctoringSettings settings, - final ScreenProctoringGroup group, + final String groupUUID, final PageAction _action) { try { @@ -329,7 +321,7 @@ public PageAction openScreenProctoringTab( HttpHeaders.AUTHORIZATION, Utils.createBasicAuthHeader( settings.spsAPIKey, - this.cryptor.decrypt(settings.getSpsAPISecret()).getOrThrow())); + this.cryptor.decrypt(settings.spsAPISecret).getOr(settings.spsAPISecret))); httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); // user credential and redirect info for jwt token request in body - form URL encoded format @@ -340,7 +332,7 @@ public PageAction openScreenProctoringTab( .getUserPassword(); final String body = "username=" + currentUser.get().username + "&password=" + userPassword.toString() - + "&redirect=/gallery-view/" + group.uuid; + + "&redirect=/gallery-view/" + groupUUID; // apply jwt token request final HttpEntity httpEntity = new HttpEntity<>(body, httpHeaders); @@ -358,12 +350,8 @@ public PageAction openScreenProctoringTab( // Open SPS Gui redirect URL with login token (jwt token) in new browser tab final String redirectLocation = redirect.getBody() + "/jwt?token=" + tokenRequest.getBody(); - // final String script = "var win = window.open('', 'seb_screen_proctoring'); win.location.href = '"+ redirectLocation + "';"; final UrlLauncher launcher = RWT.getClient().getService(UrlLauncher.class); launcher.openURL(redirectLocation); -// RWT.getClient() -// .getService(JavaScriptExecutor.class) -// .execute(script); } catch (final Exception e) { if (e instanceof HttpClientErrorException) { if (((HttpClientErrorException) e).getRawStatusCode() == HttpStatus.UNAUTHORIZED.value()) { @@ -625,7 +613,7 @@ private void processProctorRoomActionActivation( treeItem.setForeground(active ? null : new Color(display, Constants.GREY_DISABLED)); } catch (final Exception e) { - log.warn("Failed to set Proctor-Room-Activation: ", e.getMessage()); + log.warn("Failed to set Proctor-Room-Activation: {}", e.getMessage()); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java index fc1dbc2a0..68174a551 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java @@ -28,7 +28,6 @@ import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; -import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.util.Pair; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; @@ -58,14 +57,14 @@ public ProctoringGUIService(final RestService restService) { } public void registerScreeProctoringGroupAction( - final ScreenProctoringGroup screenProctoringGroup, + final String groupUUID, final TreeItem actionItem) { - this.screenProctoringGroupState.put(screenProctoringGroup.uuid, actionItem); + this.screenProctoringGroupState.put(groupUUID, actionItem); } - public TreeItem getScreeProctoringGroupAction(final ScreenProctoringGroup screenProctoringGroup) { - return this.screenProctoringGroupState.get(screenProctoringGroup.uuid); + public TreeItem getScreeProctoringGroupAction(final String groupUUID) { + return this.screenProctoringGroupState.get(groupUUID); } public boolean collectingRoomActionActive(final String name) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java index c98c699db..81b1c6bb9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java @@ -64,6 +64,8 @@ default void evictConnectionTokenCache(final Long examId) { * @return Result refer to the collection of connection tokens or to an error when happened */ Result> getActiveConnectionTokens(Long examId); + boolean hasActiveSEBConnections(Long examId); + /** Get a list of all connection tokens of all connections of an exam * that are in state an active state. See ClientConnection * @@ -249,4 +251,5 @@ default void evictConnectionTokenCache(final Long examId) { * @return Result refer to the list of deleted client connections or to an error when happened */ Result> deleteAllForExam(Long examId); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java index db1a16934..e579197be 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java @@ -224,6 +224,25 @@ public Result> getActiveConnectionTokens(final Long examId) { .collect(Collectors.toList())); } + @Override + @Transactional(readOnly = true) + public boolean hasActiveSEBConnections(final Long examId) { + try { + return this.clientConnectionRecordMapper + .countByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .and( + ClientConnectionRecordDynamicSqlSupport.status, + SqlBuilder.isEqualTo(ConnectionStatus.ACTIVE.name())) + .build() + .execute() > 0; + } catch (final Exception e) { + return true; + } + } + @Override @Transactional(readOnly = true) public Result> getAllActiveConnectionTokens(final Long examId) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ProctoringSettingsDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ProctoringSettingsDAOImpl.java index 716478903..a1b526a4a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ProctoringSettingsDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ProctoringSettingsDAOImpl.java @@ -87,10 +87,7 @@ public Result getProctoringSettings(final EntityKey e throw new RuntimeException(e); } }) - .onErrorDo( error -> { - log.warn("Failed to get Live Proctoring Settings with single additional attribute store method. Try legacy method to get Live Proctoring Settings..."); - return getLegacyProctoringServiceSettings(entityKey, entityId); - } ) + .onErrorDo( error -> getLegacyProctoringServiceSettings(entityKey, entityId)) .getOrThrow(); }); } @@ -141,10 +138,7 @@ public Result getScreenProctoringSettings(final Entity throw new RuntimeException(e); } }) - .onErrorDo( error -> { - log.warn("Failed to get SPS Settings with single additional attribute store method. Try legacy method to get SPS Settings..."); - return getLegacyScreenProctoringSettings(entityKey, entityId); - } ) + .onErrorDo( error -> getLegacyScreenProctoringSettings(entityKey, entityId)) .getOrThrow(); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ScreenProctoringGroupDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ScreenProctoringGroupDAOImpl.java index 615d617d1..39052ef19 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ScreenProctoringGroupDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ScreenProctoringGroupDAOImpl.java @@ -258,7 +258,6 @@ public void updateGroupSize( } catch (final Exception e) { log.warn("Failed to update SPS group size: {}", e.getMessage()); } - } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java index cb1c7e21e..aa4ff8164 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamTemplateService.java @@ -58,6 +58,6 @@ public interface ExamTemplateService { * @return Result refer to the created exam or to an error when happened */ Result initExamConfiguration(Exam exam); - Result applyScreenProctoringSettingsForExam(Exam exam); + Result applyScreenProctoringSettingsForExam(Exam exam); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index d0499ca81..448e2fdab 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -179,19 +179,21 @@ public Result saveProctoringServiceSettings( @Override public Result isProctoringEnabled(final Long examId) { - return this.additionalAttributesDAO.getAdditionalAttribute( - EntityType.EXAM, - examId, - ProctoringServiceSettings.ATTR_ENABLE_PROCTORING) - .map(rec -> BooleanUtils.toBoolean(rec.getValue())) - .onErrorDo(error -> { - if (log.isDebugEnabled()) { - log.warn("Failed to verify proctoring enabled for exam: {}, {}", - examId, - error.getMessage()); - } - return false; - }); + // Live Proctoring is disabled + return Result.of(false); +// return this.additionalAttributesDAO.getAdditionalAttribute( +// EntityType.EXAM, +// examId, +// ProctoringServiceSettings.ATTR_ENABLE_PROCTORING) +// .map(rec -> BooleanUtils.toBoolean(rec.getValue())) +// .onErrorDo(error -> { +// if (log.isDebugEnabled()) { +// log.warn("Failed to verify proctoring enabled for exam: {}, {}", +// examId, +// error.getMessage()); +// } +// return false; +// }); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java index 5665ee5e2..909ffa814 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java @@ -8,15 +8,13 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; + +import java.util.*; +import java.util.stream.Collectors; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.*; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ProctoringAdminService; -import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -258,22 +256,80 @@ public Result applyScreenProctoringSettingsForExam(final Exam exam) { return Result.of(exam); } - return proctoringAdminService - .getScreenProctoringSettings(new EntityKey(exam.examTemplateId, EntityType.EXAM_TEMPLATE)) - .map(settings -> { - if (BooleanUtils.isTrue(settings.enableScreenProctoring)) { - proctoringAdminService + return Result.tryCatch(() -> { + final ExamTemplate examTemplate = this.examTemplateDAO + .byPK(exam.examTemplateId) + .onError(error -> log.warn("No exam template found for id: {} error: {}", + exam.examTemplateId, + error.getMessage())) + .getOrThrow(); + + + final Result screenProctoringSettings = proctoringAdminService + .getScreenProctoringSettings(new EntityKey(exam.examTemplateId, EntityType.EXAM_TEMPLATE)); + + if (!screenProctoringSettings.hasError()) { + return screenProctoringSettings + .map(settings -> convertSPSTemplateSettings(exam, examTemplate, settings)) + .map(settings -> proctoringAdminService .saveScreenProctoringSettings(exam.getEntityKey(), settings) - .getOrThrow(); - } - return settings; - }) - - .onError(error -> log.warn( - "Failed to apply screen proctoring settings from Exam Template {} to Exam {}", - exam.examTemplateId, - exam)) - .map(settings -> exam); + .getOrThrow()) + .map(settings -> exam) + .onError(error -> log.warn( + "Failed to apply screen proctoring settings from Exam Template {} to Exam {} cause: {}", + exam.examTemplateId, + exam, + error.getMessage())) + .getOr(exam); + } else { + log.debug("No Screen Proctoring settings found for Exam Template: {}", examTemplate); + return exam; + } + }); + } + + private ScreenProctoringSettings convertSPSTemplateSettings( + final Exam exam, + final ExamTemplate examTemplate, + final ScreenProctoringSettings screenProctoringSettings) { + if (screenProctoringSettings.collectingStrategy == CollectingStrategy.APPLY_SEB_GROUPS) { + // in this case we need to map the selected template client groups to the just created exam client groups + final Set selectedTemplateIds = Arrays.stream(StringUtils.split( + screenProctoringSettings.sebGroupsSelection, + Constants.LIST_SEPARATOR_CHAR)) + .map(Long::valueOf) + .collect(Collectors.toSet()); + + final List selectedNames = examTemplate.clientGroupTemplates + .stream() + .filter(gt -> selectedTemplateIds.contains(gt.id)) + .map(gt -> gt.name) + .toList(); + + final List selectedInstances = clientGroupDAO + .allForExam(exam.id) + .getOr(Collections.emptyList()) + .stream() + .filter(g -> selectedNames.contains(g.name)) + .map(g -> String.valueOf(g.id)) + .toList(); + + return new ScreenProctoringSettings( + screenProctoringSettings.examId, + screenProctoringSettings.enableScreenProctoring, + screenProctoringSettings.spsServiceURL, + screenProctoringSettings.spsAPIKey, + screenProctoringSettings.spsAPISecret, + screenProctoringSettings.spsAccountId, + screenProctoringSettings.spsAccountPassword, + screenProctoringSettings.collectingStrategy, + screenProctoringSettings.collectingGroupName, + screenProctoringSettings.collectingGroupSize, + StringUtils.join(selectedInstances, Constants.LIST_SEPARATOR), + screenProctoringSettings.bundled + ); + } + return screenProctoringSettings; } private ConfigurationNode createOrReuseConfig(final Exam exam, final ExamTemplate examTemplate) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java index fbf2141e2..3c1b4804c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java @@ -84,7 +84,7 @@ public Result saveProctoringServiceSettings( if (parentEntityKey.entityType == EntityType.EXAM) { try { - this.examSessionCacheService.evict(Long.parseLong(parentEntityKey.modelId)); + this.examSessionCacheService.evictScreenProctoringGroups(Long.parseLong(parentEntityKey.modelId)); } catch (final Exception e) { log.warn("Failed to update Exam cache:_{}", e.getMessage()); } @@ -133,7 +133,7 @@ public Result saveScreenProctoringSettings( } this.screenProctoringService - .testSettings(settings) + .testSettings(settings, parentEntityKey) .flatMap(s -> this.proctoringSettingsDAO.storeScreenProctoringSettings(parentEntityKey, s)) .getOrThrow(); @@ -144,9 +144,10 @@ public Result saveScreenProctoringSettings( .onError(error -> this.proctoringSettingsDAO .disableScreenProctoring(screenProctoringSettings.examId)) .getOrThrow(); - + + // TODO this needs to be done elsewhere. Do proper Exam cache flush on SPS Settings save try { - this.examSessionCacheService.evict(Long.parseLong(parentEntityKey.modelId)); + this.examSessionCacheService.evictScreenProctoringGroups(settings.examId); } catch (final Exception e) { log.warn("Failed to update Exam cache:_{}", e.getMessage()); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java index 6a7e73092..b675a87a7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java @@ -12,6 +12,7 @@ import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.session.ProctoringGroupMonitoringData; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsSetupChangeEvent; import org.springframework.context.event.EventListener; @@ -41,8 +42,9 @@ default void processSessionUpdateTask() { * connect to the given SEB screen proctoring service. * * @param settings ScreenProctoringSettings + * @param parentKey the modelId of the parent Exam or ExamTemplate * @return Result refer to the settings or to an error when happened */ - Result testSettings(ScreenProctoringSettings settings); + Result testSettings(ScreenProctoringSettings settings, EntityKey parentKey); /** This applies the stored screen proctoring for the given exam. * If screen proctoring for the exam is enabled, this initializes or re-activate all @@ -53,8 +55,12 @@ default void processSessionUpdateTask() { * @param entityKey use the screen proctoring settings of the exam with the given exam id * @return Result refer to the given Exam or to an error when happened */ Result applyScreenProctoringForExam(EntityKey entityKey); + + Result> getCollectingGroupsMonitoringData(final Long examId); - /** Get list of all screen proctoring collecting groups for a particular exam. + /** Get map of all screen proctoring collecting groups for a particular exam. + * The ScreenProctoringGroup is mapped to its uuids. + * The groups are get from cache if available and load to cache if not * * @param examId The exam identifier (PK) * @return Result refer to the list of ScreenProctoringGroup or to an error when happened */ @@ -104,8 +110,7 @@ default void processSessionUpdateTask() { @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) void synchronizeSPSUser(final String userUUID); - - + @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) void synchronizeSPSUserForExam(final Long examId); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java index 063c37a5b..eac6ed283 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java @@ -10,7 +10,12 @@ import java.io.ByteArrayOutputStream; import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import ch.ethz.seb.sebserver.gbl.model.session.ProctoringGroupMonitoringData; import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*; import org.slf4j.Logger; @@ -107,19 +112,6 @@ public Exam evict(final Exam exam) { return exam; } - @CacheEvict( - cacheNames = CACHE_NAME_RUNNING_EXAM, - key = "#examId") - public Long evict(final Long examId) { - - if (log.isTraceEnabled()) { - log.trace("Conditional eviction of running Exam from cache: {}", examId); - } - - this.clientGroupDAO.evictCacheForExam(examId); - return examId; - } - public boolean isRunning(final Exam exam) { if (exam == null || !exam.active) { return false; @@ -160,29 +152,36 @@ public void evictClientConnection(final String connectionToken) { log.trace("Eviction of ClientConnectionData from cache: {}", connectionToken); } } - - @Cacheable( - cacheNames = CACHE_NAME_SCREEN_PROCTORING_GROUPS, - key = "#examId", - unless = "#result == null") - public Result> getScreenProctoringGroups(final Long examId) { - final Result> result = screenProctoringGroupDAO + +// TODO currently caching is not enabled because difficulty with distributed setup and size update task on master +// @Cacheable( +// cacheNames = CACHE_NAME_SCREEN_PROCTORING_GROUPS, +// key = "#examId", +// unless = "#result == null") + public Result> getScreenProctoringGroups(final Long examId) { + + // TODO get it directly from new DAO method + final Result> result = screenProctoringGroupDAO .getCollectingGroups(examId) + .map(list -> (Collection) list + .stream() + .map(g -> new ProctoringGroupMonitoringData(g.uuid, g.name, g.size)) + .toList()) .onError(error -> log.error( "Failed to screen proctoring groups for exam: {}, cause: {}", examId, error.getMessage())); - + if (result.hasError()) { return null; } - + return result; } - @CacheEvict( - cacheNames = CACHE_NAME_SCREEN_PROCTORING_GROUPS, - key = "#examId") +// @CacheEvict( +// cacheNames = CACHE_NAME_SCREEN_PROCTORING_GROUPS, +// key = "#examId") public void evictScreenProctoringGroups(final Long examId) { if (log.isTraceEnabled()) { log.trace("Eviction of ScreenProctoringGroups from cache for exam: {}", examId); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/SPS_API.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/SPS_API.java index 411856341..646618948 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/SPS_API.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/SPS_API.java @@ -139,6 +139,7 @@ public ExamUpdate( } } + // TODO make this more compact @JsonIgnoreProperties(ignoreUnknown = true) final class GroupSessionCount { @JsonProperty("uuid") @@ -230,7 +231,7 @@ final class ScreenProctoringServiceOAuthTemplate { CharSequence decryptedSecret = apiBinding.cryptor .decrypt(clientCredentials.secret) - .getOrThrow(); + .getOr(clientCredentials.secret); final ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails(); resource.setAccessTokenUri(spsAPIAccessData.getSpsServiceURL() + TOKEN_ENDPOINT); @@ -244,7 +245,7 @@ final class ScreenProctoringServiceOAuthTemplate { decryptedSecret = apiBinding.cryptor .decrypt(userCredentials.secret) - .getOrThrow(); + .getOr(userCredentials.secret); resource.setUsername(userCredentials.clientIdAsString()); resource.setPassword(decryptedSecret.toString()); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java index 96ffa1e42..c12f0fef2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java @@ -153,6 +153,22 @@ SPSData getSPSData(final Long examId) { } } + ScreenProctoringSettings getSettingsForExam(final Exam exam) { + if (exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_ADDITIONAL_ATTRIBUTE_STORE_NAME)) { + try { + final String encrypted = exam.additionalAttributes.get(ScreenProctoringSettings.ATTR_ADDITIONAL_ATTRIBUTE_STORE_NAME); + return jsonMapper.readValue(cryptor.decrypt(encrypted).getOrThrow().toString(), ScreenProctoringSettings.class); + } catch (final Exception e) { + log.warn("Failed to parse ScreenProctoringSettings from Exam additional attributes: {}", e.getMessage()); + } + } + + // load it from DB + return this.proctoringSettingsDAO + .getScreenProctoringSettings(new EntityKey(exam.id, EntityType.EXAM)) + .getOrThrow(); + } + /** This is called when the Screen Proctoring is been enabled for an Exam * If the needed resources on SPS side has been already created before, this just reactivates * all resources on SPS side. @@ -363,17 +379,20 @@ private void synchronizeFromSEBGroups( final ScreenProctoringGroup existing = sebGroupIdMap.remove(sebGroup.id); if (existing == null) { // create new group locally as well as on SPS - createNewLocalGroup( - exam, - createGroupOnSPS( - 0, - exam.id, - existing.name, - spsData.spsExamUUID, - false, - sebGroup.id, - apiTemplate)); - + try { + createNewLocalGroup( + exam, + createGroupOnSPS( + 0, + exam.id, + sebGroup.name, + spsData.spsExamUUID, + false, + sebGroup.id, + apiTemplate)); + } catch (final Exception e) { + log.error("Failed to create SPS group while synchronizing for exam: {} group: {} cause: {}", exam, sebGroup, e.getMessage()); + } } else { // update existing group if name has changed if (!Objects.equals(existing.name, sebGroup.name)) { @@ -383,7 +402,7 @@ private void synchronizeFromSEBGroups( final SPSGroup spsGroup = spsGroups.get(existing.uuid); if (spsGroup != null) { if (!Objects.equals(spsGroup.name(), settings.collectingGroupName)) { - updateGroupOnSPS(spsData, settings, apiTemplate, spsGroup); + updateGroupOnSPS(spsData, sebGroup.name, apiTemplate, spsGroup); } } else { log.warn( @@ -461,23 +480,23 @@ private void synchronizeDefaultGroup( } // if name has changed synchronize on SPS if (!Objects.equals(spsGroup.name(), settings.collectingGroupName)) { - updateGroupOnSPS(spsData, settings, apiTemplate, spsGroup); + updateGroupOnSPS(spsData, settings.collectingGroupName, apiTemplate, spsGroup); } } private void updateGroupOnSPS( final SPSData spsData, - final ScreenProctoringSettings settings, + final String name, final ScreenProctoringServiceOAuthTemplate apiTemplate, final SPSGroup spsGroup) { final String groupRequestURI = UriComponentsBuilder .fromUriString(apiTemplate.spsAPIAccessData.getSpsServiceURL()) - .path(GROUP_BY_EXAM_ENDPOINT) - .pathSegment(spsData.spsExamUUID) + .path(GROUP_ENDPOINT) + .pathSegment(spsGroup.uuid()) .build().toUriString(); final Map values = Map.of( - "name", settings.collectingGroupName, + "name", name, "description", spsGroup.description() ); try { @@ -550,12 +569,15 @@ private Collection getSPSGroups( final String groupRequestURI = UriComponentsBuilder .fromUriString(apiTemplate.spsAPIAccessData.getSpsServiceURL()) - .path(GROUP_ENDPOINT) + .path(GROUP_BY_EXAM_ENDPOINT) .pathSegment(spsExamUUID) .build() .toUriString(); final ResponseEntity exchangeGroups = apiTemplate.exchange(groupRequestURI, HttpMethod.GET); - if (exchangeGroups.getStatusCode() != HttpStatus.OK) { + if (exchangeGroups.getStatusCode() == HttpStatus.NOT_FOUND) { + log.info("No SPS Groups found for exam: {} on SPS", exam); + return Collections.emptyList(); + } else if (exchangeGroups.getStatusCode() != HttpStatus.OK) { throw new RuntimeException("Failed to get groups for exam from SPS. Status: " + exchangeGroups.getStatusCode()); } return jsonMapper.readValue( @@ -819,17 +841,19 @@ String updateSEBSession( void deleteExamOnScreenProctoring(final Exam exam) { try { - - if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { - return; - } - + if (log.isDebugEnabled()) { - log.debug("Deactivate exam and groups on SPS site and send deletion request for exam {}", exam); + log.info("Delete or deactivate exam and groups on SPS site and send deletion request for exam {}", exam); } final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); final SPSData spsData = this.getSPSData(exam.id); + + if (spsData == null) { + log.info("There os no SPS data for this exam"); + return; + } + deletion(SEB_ACCESS_ENDPOINT, spsData.spsSEBAccessUUID, apiTemplate); activation(exam, EXAM_ENDPOINT, spsData.spsExamUUID, false, apiTemplate); @@ -858,7 +882,6 @@ void deleteExamOnScreenProctoring(final Exam exam) { } catch (final Exception e) { log.warn("Failed to apply SPS deletion of exam: {} error: {}", exam, e.getMessage()); } - return; } public Collection getActiveGroupSessionCounts() { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java index 9b22a6170..bf0239397 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java @@ -11,13 +11,18 @@ import static ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_SCREEN_PROCTORING.*; import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.session.ProctoringGroupMonitoringData; import ch.ethz.seb.sebserver.gbl.monitoring.ClientGroupMatcherService; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*; @@ -120,8 +125,16 @@ public boolean isScreenProctoringEnabled(final Long examId) { } @Override - public Result testSettings(final ScreenProctoringSettings screenProctoringSettings) { + public Result testSettings( + final ScreenProctoringSettings screenProctoringSettings, + final EntityKey parentKey) { + return Result.tryCatch(() -> { + + if (!BooleanUtils.isTrue(screenProctoringSettings.enableScreenProctoring)) { + log.debug("Screen Proctoring is not enabled --> not test is applied"); + return screenProctoringSettings; + } final Collection fieldChecks = new ArrayList<>(); if (StringUtils.isBlank(screenProctoringSettings.spsServiceURL)) { @@ -156,7 +169,61 @@ public Result testSettings(final ScreenProctoringSetti "clientSecret", "screenProctoringSettings:spsAccountPassword:notNull")); } + + if (screenProctoringSettings.collectingStrategy == CollectingStrategy.APPLY_SEB_GROUPS && + StringUtils.isBlank(screenProctoringSettings.sebGroupsSelection)) { + fieldChecks.add(APIMessage.fieldValidationError( + "clientSecret", + "screenProctoringSettings:spsSEBGroupsSelection:notNull")); + } + + if (parentKey.entityType == EntityType.EXAM) { + + if (this.clientConnectionDAO.hasActiveSEBConnections(screenProctoringSettings.examId)) { + throw new APIMessageException(APIMessage.ErrorMessage.CLIENT_CONNECTION_INTEGRITY_VIOLATION.of()); + } + // get existing groups + final Collection existingGroups = this + .getCollectingGroups(screenProctoringSettings.examId) + .getOr(Collections.emptyList()); + + if (!existingGroups.isEmpty()) { + // check for when there are already existing groups + final ScreenProctoringSettings oldSettings = this.proctoringSettingsDAO + .getScreenProctoringSettings(parentKey) + .getOr(null); + + if (oldSettings != null && oldSettings.collectingStrategy != screenProctoringSettings.collectingStrategy) { + // not possible to change grouping strategy when it has already groups + fieldChecks.add(APIMessage.fieldValidationError( + "spsCollectingStrategy", + "screenProctoringSettings:spsCollectingStrategy:collecting-strategy-not-changeable")); + } + + // find deletion and check possibility (only deletable if no sessions) + if (screenProctoringSettings.collectingStrategy == CollectingStrategy.APPLY_SEB_GROUPS) { + final Map existing = existingGroups.stream() + .filter(g -> !g.isFallback) + .collect(Collectors.toMap( g -> g.sebGroupId, Function.identity())); + + Arrays.stream(StringUtils.split( + screenProctoringSettings.sebGroupsSelection, + Constants.LIST_SEPARATOR_CHAR)) + .map(Long::valueOf) + .forEach(existing::remove); + + existing.values().forEach( g -> { + if (g.size != null && g.size > 0) { + fieldChecks.add(APIMessage.fieldValidationError( + "clientSecret", + "screenProctoringSettings:spsSEBGroupsSelection:group-not-deletable")); + } + }); + } + } + } + if (!fieldChecks.isEmpty()) { throw new APIMessageException(fieldChecks); } @@ -212,15 +279,26 @@ public Result applyScreenProctoringForExam(final EntityKey entityKey) { } else if (isEnabling) { this.screenProctoringAPIBinding.synchronizeGroups(exam); } + return exam; }); } - + @Override - public Result> getCollectingGroups(final Long examId) { + public Result> getCollectingGroupsMonitoringData(final Long examId) { return this.examSessionCacheService.getScreenProctoringGroups(examId); } + @Override + public Result> getCollectingGroups(final Long examId) { + return screenProctoringGroupDAO + .getCollectingGroups(examId) + .onError(error -> log.error( + "Failed to screen proctoring groups for exam: {}, cause: {}", + examId, + error.getMessage())); + } + @Override public Result updateExamOnScreenProctoringService(final Long examId) { return this.examDAO.byPK(examId) @@ -258,19 +336,19 @@ public void updateClientConnections() { public void updateActiveGroups() { try { + // only if (!webserviceInfo.isMaster()) { return; } + // TODO make this more performant (batch update, check if size has changed, caching... if (screenProctoringGroupDAO.hasActiveGroups()) { screenProctoringAPIBinding .getActiveGroupSessionCounts() - .forEach(groupCount -> { - screenProctoringGroupDAO.updateGroupSize( - groupCount.groupUUID, - groupCount.activeCount, - groupCount.totalCount); - }); + .forEach(groupCount -> screenProctoringGroupDAO.updateGroupSize( + groupCount.groupUUID, + groupCount.activeCount, + groupCount.totalCount)); } } catch (final Exception e) { log.warn("Failed to update actual group session counts."); @@ -399,7 +477,6 @@ public void notifyLmsSetupChange(final LmsSetupChangeEvent event) { private void applyScreenProctoringSession(final ClientConnectionRecord ccRecord) { - try { final Long examId = ccRecord.getExamId(); final Exam runningExam = this.examSessionCacheService.getRunningExam(examId); @@ -434,7 +511,7 @@ private void applyScreenProctoringSession(final ClientConnectionRecord ccRecord) .getOrThrow(); } catch (final Exception e) { - log.error("Failed to apply screen proctoring session to SEB with connection: {}", ccRecord, e); + log.error("Failed to apply screen proctoring session to SEB with connection: {} cause: {}", ccRecord, e.getMessage()); } } @@ -442,15 +519,9 @@ private ScreenProctoringGroup applySEBConnectionToGroup( final ClientConnectionRecord ccRecord, final Exam exam) { - if (!exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY)) { - log.warn("Can't verify collecting strategy for exam: {} use default group assignment.", exam.id); - return applyToDefaultGroup(ccRecord.getId(), ccRecord.getConnectionToken(), exam); - } - - final CollectingStrategy strategy = CollectingStrategy.valueOf(exam.additionalAttributes - .get(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY)); + final ScreenProctoringSettings settingsForExam = this.screenProctoringAPIBinding.getSettingsForExam(exam); - return switch (strategy) { + return switch (settingsForExam.collectingStrategy) { case APPLY_SEB_GROUPS -> applyToSEBClientGroup(ccRecord, exam); case EXAM -> applyToDefaultGroup(ccRecord.getId(), ccRecord.getConnectionToken(), exam); }; @@ -473,7 +544,10 @@ private ScreenProctoringGroup applyToDefaultGroup( if (groups.size() == 1) { screenProctoringGroup = groups.iterator().next(); } else { - groups.stream().filter(group -> BooleanUtils.isTrue(group.isFallback)).findFirst().orElseGet(null); + screenProctoringGroup = groups.stream() + .filter(group -> BooleanUtils.isTrue(group.isFallback)) + .findFirst() + .orElseGet(null); } if (screenProctoringGroup == null) { @@ -502,7 +576,7 @@ private ScreenProctoringGroup applyToSEBClientGroup( getCollectingGroups(exam.id) .getOrThrow(); - for (ScreenProctoringGroup group : groups) { + for (final ScreenProctoringGroup group : groups) { if (group.sebGroupId != null) { final ClientGroup clientGroup = clientGroupDAO .byPK(group.sebGroupId) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index 4b267991c..5a0cf08ac 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; +import ch.ethz.seb.sebserver.gbl.model.session.*; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.session.*; import org.apache.commons.lang3.BooleanUtils; @@ -53,11 +54,6 @@ import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionIssueStatus; -import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; -import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; -import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; -import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; -import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; @@ -88,7 +84,6 @@ public class ExamMonitoringController { private final AuthorizationService authorization; private final PaginationService paginationService; private final SEBClientNotificationService sebClientNotificationService; - private final RemoteProctoringRoomService examProctoringRoomService; private final ExamAdminService examAdminService; private final SecurityKeyService securityKeyService; private final ScreenProctoringService screenProctoringService; @@ -102,7 +97,6 @@ public ExamMonitoringController( final AuthorizationService authorization, final PaginationService paginationService, final SEBClientNotificationService sebClientNotificationService, - final RemoteProctoringRoomService examProctoringRoomService, final SecurityKeyService securityKeyService, final ExamAdminService examAdminService, final ScreenProctoringService screenProctoringService, @@ -116,7 +110,6 @@ public ExamMonitoringController( this.authorization = authorization; this.paginationService = paginationService; this.sebClientNotificationService = sebClientNotificationService; - this.examProctoringRoomService = examProctoringRoomService; this.examAdminService = examAdminService; this.securityKeyService = securityKeyService; this.screenProctoringService = screenProctoringService; @@ -375,25 +368,15 @@ public MonitoringFullPageData getFullMonitoringPageData( final boolean proctoringEnabled = this.examAdminService.isProctoringEnabled(runningExam); final boolean screenProctoringEnabled = this.examAdminService.isScreenProctoringEnabled(runningExam); - -// final Collection proctoringData = (proctoringEnabled) -// ? this.examProctoringRoomService -// .getProctoringCollectingRooms(examId) -// .onError(error -> log.error("Failed to get RemoteProctoringRoom for exam: {}", examId, error)) -// .getOr(Collections.emptyList()) -// : Collections.emptyList(); - - final Collection screenProctoringData = (screenProctoringEnabled) + final Collection screenProctoringData = (screenProctoringEnabled) ? this.screenProctoringService - .getCollectingGroups(examId) - .onError(error -> log.error("Failed to get ScreenProctoringGroup for exam: {}", examId, error)) - .getOr(Collections.emptyList()) + .getCollectingGroupsMonitoringData(examId) + .getOr(Collections.emptyList()) : Collections.emptyList(); return new MonitoringFullPageData( examId, monitoringSEBConnectionData, - Collections.emptyList(), screenProctoringData); } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index eb670b6b7..fa59c6454 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -124,6 +124,9 @@ sebserver.form.validation.fieldError.url.noAccess=Access was denied sebserver.form.validation.fieldError.invalidDateRange=Invalid Date Range sebserver.form.validation.fieldError.endBeforeStart=Invalid Date Range, End before Start sebserver.form.validation.fieldError.changeDeniedActiveClients=Cannot be changed while there are active SEB client connection +sebserver.form.validation.fieldError.collecting-strategy-not-changeable=Not changeable once created +sebserver.form.validation.fieldError.group-not-deletable=Existing groups with data are not deletable +sebserver.form.validation.fieldError.active-clients=Y sebserver.error.unexpected=Unexpected Error sebserver.page.message=Information sebserver.dialog.confirm.title=Confirmation @@ -798,6 +801,8 @@ sebserver.exam.clientgroup.form.title.subtitle= sebserver.exam.clientgroup.form.title.new=Add Client Group sebserver.exam.clientgroup.form.exam=Exam sebserver.exam.clientgroup.form.exam.tooltip=The exam this client group belongs to +sebserver.exam.clientgroup.form.exam-template=Exam Template +sebserver.exam.clientgroup.form.exam-template.tooltip=The exam template this client group belongs to sebserver.exam.clientgroup.form.name=Name sebserver.exam.clientgroup.form.name.tooltip=The name of the client group.

This name is also displayed in the column cell of in the exam monitoring sebserver.exam.clientgroup.form.type=Type @@ -944,6 +949,7 @@ sebserver.exam.sps.form.clientgroups=SEB Client Groups sebserver.exam.sps.form.clientgroups.tooltip=Please select the SEB Client Groups that should be mapped to screen proctoring groups for this exam sebserver.exam.sps.form.fallback.group.name=Fallback Group Name sebserver.exam.sps.form.fallback.group.name.tooltip=SEB Server creates a fallback group for SEB client connection that does not match any of the selected SEB client groups. +sebserver.exam.sps.form.active-seb-clients=It is not possible to change Screen Proctoring settings as long as there are active SEB Clients connected to the Exam.
Please close, quit or cancel all active SEB Client connections for this Exam within the Exam Monitoring. sebserver.exam.signaturekey.action.edit=App Signature Key sebserver.exam.signaturekey.action.save=Save Settings @@ -2259,7 +2265,7 @@ sebserver.monitoring.exam.list.actions= sebserver.monitoring.exam.action.detail.view=Back To Monitoring sebserver.monitoring.exam.action.list.view=Monitoring sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} ) -sebserver.monitoring.exam.action.viewgroup=View {0} ( {1} ) +sebserver.monitoring.exam.action.viewgroup={0} ( {1} ) sebserver.exam.monitoring.action.category.statefilter=State Filter sebserver.exam.monitoring.action.category.groupfilter=Client Group Filter sebserver.exam.monitoring.action.category.issuefilter=Issue Filter diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/monitoring/AlphabeticalNameRangeMatcherTest.java b/src/test/java/ch/ethz/seb/sebserver/gbl/monitoring/AlphabeticalNameRangeMatcherTest.java new file mode 100644 index 000000000..3f8286e74 --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/gbl/monitoring/AlphabeticalNameRangeMatcherTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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.gbl.monitoring; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class AlphabeticalNameRangeMatcherTest { + + @Test + public void test1() { + final AlphabeticalNameRangeMatcher alphabeticalNameRangeMatcher = new AlphabeticalNameRangeMatcher(); + + final String start1 = "A"; + final String end1 = "M"; + + assertTrue(alphabeticalNameRangeMatcher.isInRange("A", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("a", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("aa", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Aa", start1, end1)); + + assertTrue(alphabeticalNameRangeMatcher.isInRange("M", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("m", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("mm", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Mm", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Mz", start1, end1)); + + assertFalse(alphabeticalNameRangeMatcher.isInRange("N", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("n", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("nn", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("NM", start1, end1)); + + } + + @Test + public void test2() { + final AlphabeticalNameRangeMatcher alphabeticalNameRangeMatcher = new AlphabeticalNameRangeMatcher(); + + final String start1 = "Andi"; + final String end1 = "Marcel"; + + // everything that do not start with the exact given start word do not fit in the range + assertFalse(alphabeticalNameRangeMatcher.isInRange("A", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("a", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("aa", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("Aa", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("An", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("And", start1, end1)); + + // everything that starts with the exact given start word fits in the range no mather what follows + assertTrue(alphabeticalNameRangeMatcher.isInRange("Andi", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Andia", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Andi*", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("AndiA", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Andi Soundso", start1, end1)); + + // everything that is below the exact given end word fits in the range + assertTrue(alphabeticalNameRangeMatcher.isInRange("M", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("m", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("ma", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Ma", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Mar", start1, end1)); + // NOTE: space is lower than "l" so this fits into range too + assertTrue(alphabeticalNameRangeMatcher.isInRange("Marce ", start1, end1)); + + // the exact given end word fits in the range + assertTrue(alphabeticalNameRangeMatcher.isInRange("Marcel", start1, end1)); + // everything that starts with the exact given word fits in the range no mather what follows + assertTrue(alphabeticalNameRangeMatcher.isInRange("Marcela", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Marcel ", start1, end1)); + assertTrue(alphabeticalNameRangeMatcher.isInRange("Marcel*", start1, end1)); + + // everything that is higher than the exact given end word is not in range + // NOTE: this is the first name that fits not in rage + assertFalse(alphabeticalNameRangeMatcher.isInRange("Marcem", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("mm", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("Mm", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("Mz", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("N", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("n", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("nn", start1, end1)); + assertFalse(alphabeticalNameRangeMatcher.isInRange("NM", start1, end1)); + + } +} diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index a44d51a92..bc69b6093 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -3231,6 +3231,9 @@ public void testUsecase22_ExamTemplate() { .withFormParam(Domain.EXAM.ATTR_EXAM_TEMPLATE_ID, savedTemplate.getModelId()) .call(); + if (newExamResult.hasError()) { + System.out.println(newExamResult.getError()); + } assertFalse(newExamResult.hasError()); final Exam exam = newExamResult.get(); assertEquals("Demo Quiz 6 (MOCKUP)", exam.name); diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java deleted file mode 100644 index 8d0827523..000000000 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2021 ETH Zürich, IT Services - * - * 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.integration.api.admin; - -import static org.junit.Assert.*; - -import java.util.Collection; - -import ch.ethz.seb.sebserver.gbl.util.Utils; -import org.junit.Before; -import org.junit.Test; -import org.junit.jupiter.api.Order; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.jdbc.Sql; - -import ch.ethz.seb.sebserver.gbl.model.exam.Exam; -import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; -import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; -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.util.Result; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService; - -@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" }) -public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationTester { - - private static final String CONNECTION_TOKEN_1 = "connection_token1"; - private static final String CONNECTION_TOKEN_2 = "connection_token2"; - - @Autowired - private RemoteProctoringRoomService examProctoringRoomService; - @Autowired - private ExamSessionService examSessionService; - @Autowired - private ExamAdminService examAdminService; - @Autowired - private ClientConnectionDAO clientConnectionDAO; - @Autowired - private ExamDAO examDAO; - @Autowired - private LmsAPIService lmsAPIService; - - @Before - public void init() { - final LmsAPITemplate lmsAPITemplate = this.lmsAPIService.getLmsAPITemplate(1L).getOrThrow(); - this.examDAO.updateQuizData(2L, lmsAPITemplate.getQuiz("quiz6").getOrThrow(), "testUpdate"); - } - - @Test - @Order(1) - public void test01_checkExamRunning() { - final Result> runningExamsForInstitution = - this.examSessionService.getRunningExams(1L, Utils.truePredicate()); - assertFalse(runningExamsForInstitution.hasError()); - final Collection collection = runningExamsForInstitution.get(); - assertFalse(collection.isEmpty()); - final Exam exam = collection.stream().filter(e -> e.id == 2L).findAny().orElse(null); - assertNotNull(exam); - assertEquals("Demo Quiz 6 (MOCKUP)", exam.name); - assertEquals("2", String.valueOf(exam.id)); - } - - @Test - @Order(2) - public void test02_setProctoringServiceSettings() { - final Result result = this.examAdminService.saveProctoringServiceSettings( - 2L, - new ProctoringServiceSettings( - 2L, true, ProctoringServerType.JITSI_MEET, "", 1, null, false, - "app-key", "app.secret", "accountId", - "clientId", - "clientSecret", "sdk-key", "sdk.secret", false)); - - assertTrue(result.hasError()); - final Exception error = result.getError(); - - assertFalse(this.examAdminService.isProctoringEnabled(2L).get()); - - // only with URL set it is saved - this.examAdminService.saveProctoringServiceSettings( - 2L, - new ProctoringServiceSettings( - 2L, true, ProctoringServerType.JITSI_MEET, "https://test.ch", 1, null, false, - "app-key", "app.secret", "accountId", - "clientId", - "clientSecret", "sdk-key", "sdk.secret", false)); - - assertTrue(this.examAdminService.isProctoringEnabled(2L).get()); - } - - @Test - @Order(3) - public void test03_addClientConnection() { - final Result createNew = this.clientConnectionDAO.createNew(new ClientConnection( - null, - 1L, - 2L, - ConnectionStatus.CONNECTION_REQUESTED, - CONNECTION_TOKEN_1, - "", - "", - "", - null, - null, - false, false)); - assertFalse(createNew.hasError()); - } - -}