SEBSERV-419 implementation

This commit is contained in:
anhefti 2024-06-27 16:21:14 +02:00
parent 8539da1879
commit 41ce1bc268
15 changed files with 207 additions and 82 deletions

View file

@ -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 + "}";

View file

@ -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<ExamStatus> ACTIVE_STATES = EnumSet.of(
ExamStatus.UP_COMING,
ExamStatus.TEST_RUN,
ExamStatus.RUNNING);
public static final List<String> ACTIVE_STATE_NAMES = Arrays.asList(
ExamStatus.UP_COMING.name(),
ExamStatus.TEST_RUN.name(),
ExamStatus.RUNNING.name());
@JsonProperty(EXAM.ATTR_ID)
public final Long id;

View file

@ -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,

View file

@ -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<Exam> 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;
}
}

View file

@ -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<Exam> {
public ToggleTestRun() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM,
new TypeReference<Exam>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT
+ API.EXAM_MONITORING_TEST_RUN_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT);
}
}

View file

@ -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<String> 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;

View file

@ -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<String> 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<ExamRecord> 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<String> 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))),

View file

@ -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")) {

View file

@ -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<Exam> toggleTestRun(Exam exam);
}

View file

@ -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:
*
* <p>
* - 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(

View file

@ -265,9 +265,10 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final FilterMap filterMap,
final Predicate<Exam> 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<Exam> 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<ClientConnectionData> 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<ClientGroup> groups = this.clientGroupDAO.allForExam(examId).getOr(null);
final Map<Long, Integer> 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<? extends ClientMonitoringDataView> 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<Long> 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;

View file

@ -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,

View file

@ -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));
}
}

View file

@ -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<RemoteProctoringRoom> proctoringData = (proctoringEnabled)
? this.examProcotringRoomService
? this.examProctoringRoomService
.getProctoringCollectingRooms(examId)
.onError(error -> log.error("Failed to get RemoteProctoringRoom for exam: {}", examId, error))
.getOr(Collections.emptyList())

View file

@ -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.<br/>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