diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 733ae229..50e00ece 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -255,7 +255,7 @@ public final class API { public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states"; public static final String EXAM_MONITORING_CLIENT_GROUP_FILTER = "hidden-client-group"; public static final String EXAM_MONITORING_ISSUE_FILTER = "hidden-issues"; - + public static final String EXAM_MONITORING_TEST_RUN_ENDPOINT = "/testrun"; public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams"; public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT = "/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 77039deb..782687a5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -8,12 +8,7 @@ package ch.ethz.seb.sebserver.gbl.model.exam; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import javax.validation.constraints.NotNull; @@ -68,6 +63,7 @@ public final class Exam implements GrantEntity { public enum ExamStatus { UP_COMING, + TEST_RUN, RUNNING, FINISHED, ARCHIVED @@ -80,6 +76,16 @@ public final class Exam implements GrantEntity { VDI } + public static final EnumSet ACTIVE_STATES = EnumSet.of( + ExamStatus.UP_COMING, + ExamStatus.TEST_RUN, + ExamStatus.RUNNING); + + public static final List ACTIVE_STATE_NAMES = Arrays.asList( + ExamStatus.UP_COMING.name(), + ExamStatus.TEST_RUN.name(), + ExamStatus.RUNNING.name()); + @JsonProperty(EXAM.ATTR_ID) public final Long id; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index 2bfb0745..9e345c88 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -298,6 +298,16 @@ public enum ActionDefinition { ImageIcon.DELETE, PageStateDefinitionImpl.EXAM_VIEW, ActionCategory.FORM), + EXAM_TOGGLE_TEST_RUN_ON( + new LocTextKey("sebserver.exam.action.test.run.on"), + ImageIcon.ARCHIVE, + PageStateDefinitionImpl.EXAM_VIEW, + ActionCategory.FORM), + EXAM_TOGGLE_TEST_RUN_OFF( + new LocTextKey("sebserver.exam.action.test.run.off"), + ImageIcon.ARCHIVE, + PageStateDefinitionImpl.EXAM_VIEW, + ActionCategory.FORM), EXAM_ARCHIVE( new LocTextKey("sebserver.exam.action.archive"), ImageIcon.ARCHIVE, 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 87205940..3daeebba 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 @@ -19,6 +19,7 @@ import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.*; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.ToggleTestRun; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.swt.layout.GridData; @@ -44,8 +45,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; -import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; -import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; @@ -68,7 +67,6 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetDefaultExamTemplate; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplate; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizData; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.ImportAsExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; @@ -195,6 +193,8 @@ public class ExamForm implements TemplateComposer { .onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error)) .getOrThrow(); + + // new PageContext with actual EntityKey final EntityKey entityKey = (readonly || !newExamNoLMS) ? pageContext.getEntityKey() : null; final PageContext formContext = pageContext.withEntityKey(exam.getEntityKey()); @@ -202,7 +202,7 @@ public class ExamForm implements TemplateComposer { final boolean isLight = pageService.isLightSetup(); final boolean modifyGrant = entityGrantCheck.m(); final boolean writeGrant = entityGrantCheck.w(); - final boolean editable = modifyGrant && (exam.getStatus() == ExamStatus.UP_COMING || exam.getStatus() == ExamStatus.RUNNING); + 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); @@ -288,6 +288,16 @@ public class ExamForm implements TemplateComposer { .withExec(this.examDeletePopup.deleteWizardFunction(pageContext)) .publishIf(() -> writeGrant && readonly) + .newAction(ActionDefinition.EXAM_TOGGLE_TEST_RUN_ON) + .withEntityKey(entityKey) + .withExec(this::toggleTestRun) + .publishIf(() -> modifyGrant && readonly && exam.status == ExamStatus.UP_COMING) + + .newAction(ActionDefinition.EXAM_TOGGLE_TEST_RUN_OFF) + .withEntityKey(entityKey) + .withExec(this::toggleTestRun) + .publishIf(() -> modifyGrant && readonly && exam.status == ExamStatus.TEST_RUN) + .newAction(ActionDefinition.EXAM_ARCHIVE) .withEntityKey(entityKey) .withConfirm(() -> EXAM_ARCHIVE_CONFIRM) @@ -399,6 +409,8 @@ public class ExamForm implements TemplateComposer { } } + + private FormHandle createReadOnlyForm( final PageContext formContext, final Composite content, @@ -806,7 +818,8 @@ public class ExamForm implements TemplateComposer { if (pageService.isLightSetup()) { mapper.putIfAbsent(Domain.EXAM.ATTR_SUPPORTER, this.pageService.getCurrentUser().get().uuid); } - return this.restService.getBuilder(GetQuizData.class) + return this.restService + .getBuilder(GetQuizData.class) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .withQueryParam(QuizData.QUIZ_ATTR_LMS_SETUP_ID, parentEntityKey.modelId) .call() @@ -833,4 +846,18 @@ public class ExamForm implements TemplateComposer { }; } + private PageAction toggleTestRun(final PageAction pageAction) { + + this.restService + .getBuilder(ToggleTestRun.class) + .withURIVariable(API.PARAM_MODEL_ID, pageAction.getEntityKey().modelId) + .call() + .onError(error -> log.error( + "Failed to toggle Test Run for exam: {}, error: {}", + pageAction.getEntityKey(), + error.getMessage())); + + return pageAction; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/ToggleTestRun.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/ToggleTestRun.java new file mode 100644 index 00000000..1b637b9e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/ToggleTestRun.java @@ -0,0 +1,39 @@ +/* + * 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.gui.service.remote.webservice.api.session; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; +import com.fasterxml.jackson.core.type.TypeReference; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +@Lazy +@Component +@GuiProfile +public class ToggleTestRun extends RestCall { + + public ToggleTestRun() { + super(new TypeKey<>( + CallType.GET_SINGLE, + EntityType.EXAM, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_MONITORING_ENDPOINT + + API.EXAM_MONITORING_TEST_RUN_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java index 9f40d5c2..7dd08afb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamConfigurationMapDAOImpl.java @@ -12,7 +12,6 @@ import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfig import static org.mybatis.dynamic.sql.SqlBuilder.*; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -37,7 +36,6 @@ import ch.ethz.seb.sebserver.gbl.client.ClientCredentialService; import ch.ethz.seb.sebserver.gbl.model.EntityDependency; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; -import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; @@ -67,10 +65,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; @WebServiceProfile public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { - private static final List ACTIVE_EXAM_STATE_NAMES = Arrays.asList( - ExamStatus.UP_COMING.name(), - ExamStatus.RUNNING.name()); - private final ExamRecordMapper examRecordMapper; private final ExamConfigurationMapRecordMapper examConfigurationMapRecordMapper; private final ConfigurationNodeRecordMapper configurationNodeRecordMapper; @@ -440,7 +434,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { try { final boolean active = this.examRecordMapper.countByExample() .where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId)) - .and(ExamRecordDynamicSqlSupport.status, isIn(ACTIVE_EXAM_STATE_NAMES)) + .and(ExamRecordDynamicSqlSupport.status, isIn(Exam.ACTIVE_STATE_NAMES)) .build() .execute() >= 1; return active; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java index c1c4b05b..8dcc4119 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java @@ -205,10 +205,18 @@ public class ExamRecordDAO { final String examStatus = filterMap.getExamStatus(); if (StringUtils.isNotBlank(examStatus)) { - whereClause = whereClause - .and( - ExamRecordDynamicSqlSupport.status, - isEqualToWhenPresent(examStatus)); + if (examStatus.contains(Constants.LIST_SEPARATOR)) { + final List state_names = Arrays.asList(StringUtils.split(examStatus, Constants.LIST_SEPARATOR)); + whereClause = whereClause + .and( + ExamRecordDynamicSqlSupport.status, + isIn(state_names)); + } else { + whereClause = whereClause + .and( + ExamRecordDynamicSqlSupport.status, + isEqualToWhenPresent(examStatus)); + } } else if (stateNames != null && !stateNames.isEmpty()) { whereClause = whereClause .and( @@ -234,14 +242,12 @@ public class ExamRecordDAO { ? filterMap.getSQLWildcard(QuizData.FILTER_ATTR_NAME) : filterMap.getSQLWildcard(Domain.EXAM.ATTR_QUIZ_NAME); - final List records = whereClause + return whereClause .and( ExamRecordDynamicSqlSupport.quizName, isLikeWhenPresent(nameCriteria)) .build() .execute(); - - return records; }); } @@ -532,7 +538,7 @@ public class ExamRecordDAO { // if up-coming but running or finished final SqlCriterion upcoming = or( ExamRecordDynamicSqlSupport.status, - isEqualTo(ExamStatus.UP_COMING.name()), + isIn(ExamStatus.UP_COMING.name(), ExamStatus.TEST_RUN.name()), and( ExamRecordDynamicSqlSupport.quizStartTime, SqlBuilder.isLessThanWhenPresent(now.minus(followupTime))), diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java index d22238e0..d992a301 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java @@ -98,7 +98,8 @@ public class MockCourseAccessAPI implements CourseAccessAPI { "Starts in five minutes and ends never", DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS * 5) .toString(Constants.DEFAULT_DATE_TIME_FORMAT), - null, + DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS * 15) + .toString(Constants.DEFAULT_DATE_TIME_FORMAT), "http://lms.mockup.com/api/")); // if (webserviceInfo.hasProfile("dev")) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java index ceb2734d..06da03c7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java @@ -9,7 +9,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session; import java.io.OutputStream; -import java.security.Principal; import java.util.Collection; import java.util.Set; import java.util.function.Predicate; @@ -258,4 +257,11 @@ public interface ExamSessionService { return connection.clientConnection.status.clientActiveStatus; } + /** Toggles the exams test run state. + * If the Exam is in state Up-Coming it puts it to Test-Run state + * If the Exam is in state Test-Run it puts it back to Up-Coming + * Every other state is ignored. + * @param exam the Exam data + * @return Result refer to Exam with new state or to an exception if there was one */ + Result toggleTestRun(Exam exam); } 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 4d379f5b..a788426a 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 @@ -28,7 +28,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService; /** Handles caching for exam session and defines caching for following object: - * + *

* - Running exams (examId -> Exam) * - in-memory exam configuration (examId -> InMemorySEBConfig) * - active client connections (connectionToken -> ClientConnectionDataInternal) @@ -122,14 +122,7 @@ public class ExamSessionCacheService { return false; } - switch (exam.status) { - case RUNNING: { - return true; - } - default: { - return false; - } - } + return exam.status == Exam.ExamStatus.RUNNING || exam.status == Exam.ExamStatus.TEST_RUN; } @Cacheable( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index 7eae391a..ef121478 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -265,9 +265,10 @@ public class ExamSessionServiceImpl implements ExamSessionService { final FilterMap filterMap, final Predicate predicate) { + final String runningStateNames = ExamStatus.RUNNING.name() + Constants.LIST_SEPARATOR + ExamStatus.TEST_RUN.name(); filterMap .putIfAbsent(Exam.FILTER_ATTR_ACTIVE, Constants.TRUE_STRING) - .putIfAbsent(Exam.FILTER_ATTR_STATUS, ExamStatus.RUNNING.name()); + .putIfAbsent(Exam.FILTER_ATTR_STATUS, runningStateNames); return this.examDAO.allMatching(filterMap, predicate) .map(col -> col.stream() @@ -383,6 +384,25 @@ public class ExamSessionServiceImpl implements ExamSessionService { } } + @Override + public Result toggleTestRun(final Exam exam) { + + return Result.tryCatch(() -> { + + if (exam.status == ExamStatus.UP_COMING) { + return examDAO + .updateState(exam.id, ExamStatus.TEST_RUN, null) + .getOrThrow(); + } else if (exam.status == ExamStatus.TEST_RUN) { + return examDAO + .updateState(exam.id, ExamStatus.UP_COMING, null) + .getOrThrow(); + } + + return exam; + }); + } + @Override public Result getConnectionData(final String connectionToken) { @@ -430,9 +450,6 @@ public class ExamSessionServiceImpl implements ExamSessionService { // needed to store connection numbers per status final int[] statusMapping = new int[ConnectionStatus.values().length]; - for (int i = 0; i < statusMapping.length; i++) { - statusMapping[i] = 0; - } // needed to store connection numbers per client group too final Collection groups = this.clientGroupDAO.allForExam(examId).getOr(null); final Map clientGroupMapping = (groups != null && !groups.isEmpty()) @@ -440,10 +457,6 @@ public class ExamSessionServiceImpl implements ExamSessionService { : null; final int[] issueMapping = new int[ConnectionIssueStatus.values().length]; - for (int i = 0; i < issueMapping.length; i++) { - issueMapping[i] = 0; - } - updateClientConnections(examId); final List filteredConnections = this.clientConnectionDAO @@ -597,7 +610,7 @@ public class ExamSessionServiceImpl implements ExamSessionService { if (this.distributedSetup && currentTimeMillis - this.lastConnectionTokenCacheUpdate > this.distributedConnectionUpdate) { - // go trough all client connection and update the ones that not up to date + // go through all client connection and update the ones that not up to date this.clientConnectionDAO.evictConnectionTokenCache(examId); final Set timestamps = this.clientConnectionDAO @@ -611,7 +624,6 @@ public class ExamSessionServiceImpl implements ExamSessionService { this.clientConnectionDAO.getClientConnectionsOutOfSyc(examId, timestamps) .getOrElse(Collections::emptySet) - .stream() .forEach(this.examSessionCacheService::evictClientConnection); this.lastConnectionTokenCacheUpdate = currentTimeMillis; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java index a4a0e8c2..208109f0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java @@ -229,17 +229,6 @@ class ExamUpdateHandler implements ExamUpdateTask { }); } - @EventListener(ExamUpdateEvent.class) - void updateRunning(final ExamUpdateEvent event) { - this.examDAO - .byPK(event.examId) - .onSuccess(exam -> updateState( - exam, - DateTime.now(DateTimeZone.UTC), - this.examTimePrefix, - this.examTimeSuffix, - this.createUpdateId())); - } void updateState( final Exam exam, 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 b2808eaf..559eddcf 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 @@ -179,7 +179,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { .map(exam -> { final boolean isSPSActive = this.screenProctoringAPIBinding.isSPSActive(exam); - final boolean isEnabling = this.proctoringSettingsDAO.isScreenProctoringEnabled(exam.id); + final boolean isEnabling = this.isScreenProctoringEnabled(exam.id); if (isEnabling && !isSPSActive) { // if screen proctoring has been enabled @@ -219,10 +219,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { return this.examDAO.byPK(examId) .map(exam -> { - final String enabled = exam.additionalAttributes - .get(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING); - - if (!BooleanUtils.toBoolean(enabled)) { + if (!this.isScreenProctoringEnabled(exam.id)) { return exam; } @@ -252,10 +249,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { @Override public void notifyExamSaved(final Exam exam) { - final String enabled = exam.additionalAttributes - .get(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING); - - if (!BooleanUtils.toBoolean(enabled)) { + if (!this.isScreenProctoringEnabled(exam.id)) { return; } @@ -295,7 +289,8 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { @Override public void notifyExamStarted(final ExamStartedEvent event) { final Exam exam = event.exam; - if (BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { + if (!this.isScreenProctoringEnabled(exam.id) || + BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { return; } @@ -305,11 +300,14 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { @Override public void notifyExamFinished(final ExamFinishedEvent event) { final Exam exam = event.exam; - if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { + if (!this.isScreenProctoringEnabled(exam.id) || + !BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { return; } - this.screenProctoringAPIBinding.deactivateScreenProctoring(exam); + if (exam.status == Exam.ExamStatus.FINISHED) { + this.screenProctoringAPIBinding.deactivateScreenProctoring(exam); + } } @Override @@ -565,4 +563,5 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { ccRecord, error)); } + } 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 a49deb1c..6465307b 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,11 +22,14 @@ import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.*; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.MediaType; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.WebDataBinder; @@ -69,12 +72,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @WebServiceProfile @@ -91,10 +88,12 @@ public class ExamMonitoringController { private final AuthorizationService authorization; private final PaginationService paginationService; private final SEBClientNotificationService sebClientNotificationService; - private final RemoteProctoringRoomService examProcotringRoomService; + private final RemoteProctoringRoomService examProctoringRoomService; private final ExamAdminService examAdminService; private final SecurityKeyService securityKeyService; private final ScreenProctoringService screenProctoringService; + private final ApplicationEventPublisher applicationEventPublisher; + private final ExamDAO examDAO; private final Executor executor; public ExamMonitoringController( @@ -103,10 +102,12 @@ public class ExamMonitoringController { final AuthorizationService authorization, final PaginationService paginationService, final SEBClientNotificationService sebClientNotificationService, - final RemoteProctoringRoomService examProcotringRoomService, + final RemoteProctoringRoomService examProctoringRoomService, final SecurityKeyService securityKeyService, final ExamAdminService examAdminService, final ScreenProctoringService screenProctoringService, + final ApplicationEventPublisher applicationEventPublisher, + final ExamDAO examDAO, @Qualifier(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) final Executor executor) { this.sebClientConnectionService = sebClientConnectionService; @@ -115,10 +116,12 @@ public class ExamMonitoringController { this.authorization = authorization; this.paginationService = paginationService; this.sebClientNotificationService = sebClientNotificationService; - this.examProcotringRoomService = examProcotringRoomService; + this.examProctoringRoomService = examProctoringRoomService; this.examAdminService = examAdminService; this.securityKeyService = securityKeyService; this.screenProctoringService = screenProctoringService; + this.applicationEventPublisher = applicationEventPublisher; + this.examDAO = examDAO; this.executor = executor; } @@ -230,7 +233,8 @@ public class ExamMonitoringController { this.authorization.checkRole( institutionId, EntityType.EXAM, - UserRole.EXAM_SUPPORTER, UserRole.TEACHER, + UserRole.EXAM_SUPPORTER, + UserRole.TEACHER, UserRole.EXAM_ADMIN); final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString()); @@ -255,6 +259,41 @@ public class ExamMonitoringController { ExamAdministrationController.pageSort(sort)); } + @RequestMapping( + path = API.EXAM_MONITORING_TEST_RUN_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT, + method = RequestMethod.POST, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public Exam toggleTestRunForExam( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long examId) { + + // check overall privileges + this.authorization.checkRole( + institutionId, + EntityType.EXAM, + UserRole.EXAM_SUPPORTER, + UserRole.TEACHER, + UserRole.EXAM_ADMIN); + + return this.examDAO.byPK(examId) + .flatMap(authorization::checkModify) + .flatMap(examSessionService::toggleTestRun) + .map(exam -> { + if (exam.status == Exam.ExamStatus.TEST_RUN) { + applicationEventPublisher.publishEvent(new ExamStartedEvent(exam)); + } else if (exam.status == Exam.ExamStatus.UP_COMING) { + applicationEventPublisher.publishEvent(new ExamFinishedEvent(exam)); + } + return exam; + }) + .getOrThrow(); + } + @RequestMapping( path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, method = RequestMethod.GET, @@ -337,7 +376,7 @@ public class ExamMonitoringController { final boolean screenProctoringEnabled = this.examAdminService.isScreenProctoringEnabled(runningExam); final Collection proctoringData = (proctoringEnabled) - ? this.examProcotringRoomService + ? this.examProctoringRoomService .getProctoringCollectingRooms(examId) .onError(error -> log.error("Failed to get RemoteProctoringRoom for exam: {}", examId, error)) .getOr(Collections.emptyList()) diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 309a4aed..997ce677 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -577,6 +577,9 @@ sebserver.exam.action.sebrestriction.disable=Release SEB Lock sebserver.exam.action.sebrestriction.details=SEB Restriction Details sebserver.exam.action.createClientToStartExam=Export Exam Connection Configuration sebserver.exam.action.sebrestriction.release.confirm=You are about to release the SEB restriction lock for this exam on the Assessment Tool.
Are you sure you want to release the SEB restriction? +sebserver.exam.action.test.run.on=Apply Test Run +sebserver.exam.action.test.run.off=Disable Test Run + sebserver.exam.info.pleaseSelect=At first please select an Exam from the list @@ -668,6 +671,7 @@ sebserver.exam.type.VDI=VDI (Virtual Desktop Infrastructure) sebserver.exam.type.VDI.tooltip=Exam type specified for Virtual Desktop Infrastructure sebserver.exam.status.UP_COMING=Up Coming +sebserver.exam.status.TEST_RUN=Test Run sebserver.exam.status.RUNNING=Running sebserver.exam.status.FINISHED=Finished sebserver.exam.status.ARCHIVED=Archived