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_STATE_FILTER = "hidden-states";
public static final String EXAM_MONITORING_CLIENT_GROUP_FILTER = "hidden-client-group"; 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_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_FINISHED_ENDPOINT = "/finishedexams";
public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT = public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT =
"/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}"; "/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}";

View file

@ -8,12 +8,7 @@
package ch.ethz.seb.sebserver.gbl.model.exam; package ch.ethz.seb.sebserver.gbl.model.exam;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@ -68,6 +63,7 @@ public final class Exam implements GrantEntity {
public enum ExamStatus { public enum ExamStatus {
UP_COMING, UP_COMING,
TEST_RUN,
RUNNING, RUNNING,
FINISHED, FINISHED,
ARCHIVED ARCHIVED
@ -80,6 +76,16 @@ public final class Exam implements GrantEntity {
VDI 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) @JsonProperty(EXAM.ATTR_ID)
public final Long id; public final Long id;

View file

@ -298,6 +298,16 @@ public enum ActionDefinition {
ImageIcon.DELETE, ImageIcon.DELETE,
PageStateDefinitionImpl.EXAM_VIEW, PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM), 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( EXAM_ARCHIVE(
new LocTextKey("sebserver.exam.action.archive"), new LocTextKey("sebserver.exam.action.archive"),
ImageIcon.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.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.exam.*;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup; 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.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.layout.GridData; 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.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; 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.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.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; 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.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.GetDefaultExamTemplate;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplate; 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.GetQuizData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.ImportAsExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.ImportAsExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; 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)) .onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow(); .getOrThrow();
// new PageContext with actual EntityKey // new PageContext with actual EntityKey
final EntityKey entityKey = (readonly || !newExamNoLMS) ? pageContext.getEntityKey() : null; final EntityKey entityKey = (readonly || !newExamNoLMS) ? pageContext.getEntityKey() : null;
final PageContext formContext = pageContext.withEntityKey(exam.getEntityKey()); final PageContext formContext = pageContext.withEntityKey(exam.getEntityKey());
@ -202,7 +202,7 @@ public class ExamForm implements TemplateComposer {
final boolean isLight = pageService.isLightSetup(); final boolean isLight = pageService.isLightSetup();
final boolean modifyGrant = entityGrantCheck.m(); final boolean modifyGrant = entityGrantCheck.m();
final boolean writeGrant = entityGrantCheck.w(); 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( final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
final boolean sebRestrictionAvailable = readonly && hasSEBRestrictionAPI(exam); final boolean sebRestrictionAvailable = readonly && hasSEBRestrictionAPI(exam);
@ -288,6 +288,16 @@ public class ExamForm implements TemplateComposer {
.withExec(this.examDeletePopup.deleteWizardFunction(pageContext)) .withExec(this.examDeletePopup.deleteWizardFunction(pageContext))
.publishIf(() -> writeGrant && readonly) .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) .newAction(ActionDefinition.EXAM_ARCHIVE)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withConfirm(() -> EXAM_ARCHIVE_CONFIRM) .withConfirm(() -> EXAM_ARCHIVE_CONFIRM)
@ -399,6 +409,8 @@ public class ExamForm implements TemplateComposer {
} }
} }
private FormHandle<Exam> createReadOnlyForm( private FormHandle<Exam> createReadOnlyForm(
final PageContext formContext, final PageContext formContext,
final Composite content, final Composite content,
@ -806,7 +818,8 @@ public class ExamForm implements TemplateComposer {
if (pageService.isLightSetup()) { if (pageService.isLightSetup()) {
mapper.putIfAbsent(Domain.EXAM.ATTR_SUPPORTER, this.pageService.getCurrentUser().get().uuid); 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) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.withQueryParam(QuizData.QUIZ_ATTR_LMS_SETUP_ID, parentEntityKey.modelId) .withQueryParam(QuizData.QUIZ_ATTR_LMS_SETUP_ID, parentEntityKey.modelId)
.call() .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 static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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.EntityDependency;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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;
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.Exam.ExamType;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus;
@ -67,10 +65,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
@WebServiceProfile @WebServiceProfile
public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO { 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 ExamRecordMapper examRecordMapper;
private final ExamConfigurationMapRecordMapper examConfigurationMapRecordMapper; private final ExamConfigurationMapRecordMapper examConfigurationMapRecordMapper;
private final ConfigurationNodeRecordMapper configurationNodeRecordMapper; private final ConfigurationNodeRecordMapper configurationNodeRecordMapper;
@ -440,7 +434,7 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
try { try {
final boolean active = this.examRecordMapper.countByExample() final boolean active = this.examRecordMapper.countByExample()
.where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId)) .where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId))
.and(ExamRecordDynamicSqlSupport.status, isIn(ACTIVE_EXAM_STATE_NAMES)) .and(ExamRecordDynamicSqlSupport.status, isIn(Exam.ACTIVE_STATE_NAMES))
.build() .build()
.execute() >= 1; .execute() >= 1;
return active; return active;

View file

@ -205,10 +205,18 @@ public class ExamRecordDAO {
final String examStatus = filterMap.getExamStatus(); final String examStatus = filterMap.getExamStatus();
if (StringUtils.isNotBlank(examStatus)) { if (StringUtils.isNotBlank(examStatus)) {
whereClause = whereClause if (examStatus.contains(Constants.LIST_SEPARATOR)) {
.and( final List<String> state_names = Arrays.asList(StringUtils.split(examStatus, Constants.LIST_SEPARATOR));
ExamRecordDynamicSqlSupport.status, whereClause = whereClause
isEqualToWhenPresent(examStatus)); .and(
ExamRecordDynamicSqlSupport.status,
isIn(state_names));
} else {
whereClause = whereClause
.and(
ExamRecordDynamicSqlSupport.status,
isEqualToWhenPresent(examStatus));
}
} else if (stateNames != null && !stateNames.isEmpty()) { } else if (stateNames != null && !stateNames.isEmpty()) {
whereClause = whereClause whereClause = whereClause
.and( .and(
@ -234,14 +242,12 @@ public class ExamRecordDAO {
? filterMap.getSQLWildcard(QuizData.FILTER_ATTR_NAME) ? filterMap.getSQLWildcard(QuizData.FILTER_ATTR_NAME)
: filterMap.getSQLWildcard(Domain.EXAM.ATTR_QUIZ_NAME); : filterMap.getSQLWildcard(Domain.EXAM.ATTR_QUIZ_NAME);
final List<ExamRecord> records = whereClause return whereClause
.and( .and(
ExamRecordDynamicSqlSupport.quizName, ExamRecordDynamicSqlSupport.quizName,
isLikeWhenPresent(nameCriteria)) isLikeWhenPresent(nameCriteria))
.build() .build()
.execute(); .execute();
return records;
}); });
} }
@ -532,7 +538,7 @@ public class ExamRecordDAO {
// if up-coming but running or finished // if up-coming but running or finished
final SqlCriterion<String> upcoming = or( final SqlCriterion<String> upcoming = or(
ExamRecordDynamicSqlSupport.status, ExamRecordDynamicSqlSupport.status,
isEqualTo(ExamStatus.UP_COMING.name()), isIn(ExamStatus.UP_COMING.name(), ExamStatus.TEST_RUN.name()),
and( and(
ExamRecordDynamicSqlSupport.quizStartTime, ExamRecordDynamicSqlSupport.quizStartTime,
SqlBuilder.isLessThanWhenPresent(now.minus(followupTime))), SqlBuilder.isLessThanWhenPresent(now.minus(followupTime))),

View file

@ -98,7 +98,8 @@ public class MockCourseAccessAPI implements CourseAccessAPI {
"Starts in five minutes and ends never", "Starts in five minutes and ends never",
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS * 5) DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS * 5)
.toString(Constants.DEFAULT_DATE_TIME_FORMAT), .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/")); "http://lms.mockup.com/api/"));
// if (webserviceInfo.hasProfile("dev")) { // if (webserviceInfo.hasProfile("dev")) {

View file

@ -9,7 +9,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session; package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.Principal;
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -258,4 +257,11 @@ public interface ExamSessionService {
return connection.clientConnection.status.clientActiveStatus; 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; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService;
/** Handles caching for exam session and defines caching for following object: /** Handles caching for exam session and defines caching for following object:
* * <p>
* - Running exams (examId -> Exam) * - Running exams (examId -> Exam)
* - in-memory exam configuration (examId -> InMemorySEBConfig) * - in-memory exam configuration (examId -> InMemorySEBConfig)
* - active client connections (connectionToken -> ClientConnectionDataInternal) * - active client connections (connectionToken -> ClientConnectionDataInternal)
@ -122,14 +122,7 @@ public class ExamSessionCacheService {
return false; return false;
} }
switch (exam.status) { return exam.status == Exam.ExamStatus.RUNNING || exam.status == Exam.ExamStatus.TEST_RUN;
case RUNNING: {
return true;
}
default: {
return false;
}
}
} }
@Cacheable( @Cacheable(

View file

@ -265,9 +265,10 @@ public class ExamSessionServiceImpl implements ExamSessionService {
final FilterMap filterMap, final FilterMap filterMap,
final Predicate<Exam> predicate) { final Predicate<Exam> predicate) {
final String runningStateNames = ExamStatus.RUNNING.name() + Constants.LIST_SEPARATOR + ExamStatus.TEST_RUN.name();
filterMap filterMap
.putIfAbsent(Exam.FILTER_ATTR_ACTIVE, Constants.TRUE_STRING) .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) return this.examDAO.allMatching(filterMap, predicate)
.map(col -> col.stream() .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 @Override
public Result<ClientConnectionData> getConnectionData(final String connectionToken) { public Result<ClientConnectionData> getConnectionData(final String connectionToken) {
@ -430,9 +450,6 @@ public class ExamSessionServiceImpl implements ExamSessionService {
// needed to store connection numbers per status // needed to store connection numbers per status
final int[] statusMapping = new int[ConnectionStatus.values().length]; 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 // needed to store connection numbers per client group too
final Collection<ClientGroup> groups = this.clientGroupDAO.allForExam(examId).getOr(null); final Collection<ClientGroup> groups = this.clientGroupDAO.allForExam(examId).getOr(null);
final Map<Long, Integer> clientGroupMapping = (groups != null && !groups.isEmpty()) final Map<Long, Integer> clientGroupMapping = (groups != null && !groups.isEmpty())
@ -440,10 +457,6 @@ public class ExamSessionServiceImpl implements ExamSessionService {
: null; : null;
final int[] issueMapping = new int[ConnectionIssueStatus.values().length]; final int[] issueMapping = new int[ConnectionIssueStatus.values().length];
for (int i = 0; i < issueMapping.length; i++) {
issueMapping[i] = 0;
}
updateClientConnections(examId); updateClientConnections(examId);
final List<? extends ClientMonitoringDataView> filteredConnections = this.clientConnectionDAO final List<? extends ClientMonitoringDataView> filteredConnections = this.clientConnectionDAO
@ -597,7 +610,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
if (this.distributedSetup && if (this.distributedSetup &&
currentTimeMillis - this.lastConnectionTokenCacheUpdate > this.distributedConnectionUpdate) { 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); this.clientConnectionDAO.evictConnectionTokenCache(examId);
final Set<Long> timestamps = this.clientConnectionDAO final Set<Long> timestamps = this.clientConnectionDAO
@ -611,7 +624,6 @@ public class ExamSessionServiceImpl implements ExamSessionService {
this.clientConnectionDAO.getClientConnectionsOutOfSyc(examId, timestamps) this.clientConnectionDAO.getClientConnectionsOutOfSyc(examId, timestamps)
.getOrElse(Collections::emptySet) .getOrElse(Collections::emptySet)
.stream()
.forEach(this.examSessionCacheService::evictClientConnection); .forEach(this.examSessionCacheService::evictClientConnection);
this.lastConnectionTokenCacheUpdate = currentTimeMillis; 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( void updateState(
final Exam exam, final Exam exam,

View file

@ -179,7 +179,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
.map(exam -> { .map(exam -> {
final boolean isSPSActive = this.screenProctoringAPIBinding.isSPSActive(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 (isEnabling && !isSPSActive) {
// if screen proctoring has been enabled // if screen proctoring has been enabled
@ -219,10 +219,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
return this.examDAO.byPK(examId) return this.examDAO.byPK(examId)
.map(exam -> { .map(exam -> {
final String enabled = exam.additionalAttributes if (!this.isScreenProctoringEnabled(exam.id)) {
.get(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING);
if (!BooleanUtils.toBoolean(enabled)) {
return exam; return exam;
} }
@ -252,10 +249,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
@Override @Override
public void notifyExamSaved(final Exam exam) { public void notifyExamSaved(final Exam exam) {
final String enabled = exam.additionalAttributes if (!this.isScreenProctoringEnabled(exam.id)) {
.get(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING);
if (!BooleanUtils.toBoolean(enabled)) {
return; return;
} }
@ -295,7 +289,8 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
@Override @Override
public void notifyExamStarted(final ExamStartedEvent event) { public void notifyExamStarted(final ExamStartedEvent event) {
final Exam exam = event.exam; 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; return;
} }
@ -305,11 +300,14 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
@Override @Override
public void notifyExamFinished(final ExamFinishedEvent event) { public void notifyExamFinished(final ExamFinishedEvent event) {
final Exam exam = event.exam; 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; return;
} }
this.screenProctoringAPIBinding.deactivateScreenProctoring(exam); if (exam.status == Exam.ExamStatus.FINISHED) {
this.screenProctoringAPIBinding.deactivateScreenProctoring(exam);
}
} }
@Override @Override
@ -565,4 +563,5 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
ccRecord, ccRecord,
error)); error));
} }
} }

View file

@ -22,11 +22,14 @@ import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; 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.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.WebDataBinder; 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.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; 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.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; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@WebServiceProfile @WebServiceProfile
@ -91,10 +88,12 @@ public class ExamMonitoringController {
private final AuthorizationService authorization; private final AuthorizationService authorization;
private final PaginationService paginationService; private final PaginationService paginationService;
private final SEBClientNotificationService sebClientNotificationService; private final SEBClientNotificationService sebClientNotificationService;
private final RemoteProctoringRoomService examProcotringRoomService; private final RemoteProctoringRoomService examProctoringRoomService;
private final ExamAdminService examAdminService; private final ExamAdminService examAdminService;
private final SecurityKeyService securityKeyService; private final SecurityKeyService securityKeyService;
private final ScreenProctoringService screenProctoringService; private final ScreenProctoringService screenProctoringService;
private final ApplicationEventPublisher applicationEventPublisher;
private final ExamDAO examDAO;
private final Executor executor; private final Executor executor;
public ExamMonitoringController( public ExamMonitoringController(
@ -103,10 +102,12 @@ public class ExamMonitoringController {
final AuthorizationService authorization, final AuthorizationService authorization,
final PaginationService paginationService, final PaginationService paginationService,
final SEBClientNotificationService sebClientNotificationService, final SEBClientNotificationService sebClientNotificationService,
final RemoteProctoringRoomService examProcotringRoomService, final RemoteProctoringRoomService examProctoringRoomService,
final SecurityKeyService securityKeyService, final SecurityKeyService securityKeyService,
final ExamAdminService examAdminService, final ExamAdminService examAdminService,
final ScreenProctoringService screenProctoringService, final ScreenProctoringService screenProctoringService,
final ApplicationEventPublisher applicationEventPublisher,
final ExamDAO examDAO,
@Qualifier(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) final Executor executor) { @Qualifier(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) final Executor executor) {
this.sebClientConnectionService = sebClientConnectionService; this.sebClientConnectionService = sebClientConnectionService;
@ -115,10 +116,12 @@ public class ExamMonitoringController {
this.authorization = authorization; this.authorization = authorization;
this.paginationService = paginationService; this.paginationService = paginationService;
this.sebClientNotificationService = sebClientNotificationService; this.sebClientNotificationService = sebClientNotificationService;
this.examProcotringRoomService = examProcotringRoomService; this.examProctoringRoomService = examProctoringRoomService;
this.examAdminService = examAdminService; this.examAdminService = examAdminService;
this.securityKeyService = securityKeyService; this.securityKeyService = securityKeyService;
this.screenProctoringService = screenProctoringService; this.screenProctoringService = screenProctoringService;
this.applicationEventPublisher = applicationEventPublisher;
this.examDAO = examDAO;
this.executor = executor; this.executor = executor;
} }
@ -230,7 +233,8 @@ public class ExamMonitoringController {
this.authorization.checkRole( this.authorization.checkRole(
institutionId, institutionId,
EntityType.EXAM, EntityType.EXAM,
UserRole.EXAM_SUPPORTER, UserRole.TEACHER, UserRole.EXAM_SUPPORTER,
UserRole.TEACHER,
UserRole.EXAM_ADMIN); UserRole.EXAM_ADMIN);
final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString()); final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString());
@ -255,6 +259,41 @@ public class ExamMonitoringController {
ExamAdministrationController.pageSort(sort)); 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( @RequestMapping(
path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT,
method = RequestMethod.GET, method = RequestMethod.GET,
@ -337,7 +376,7 @@ public class ExamMonitoringController {
final boolean screenProctoringEnabled = this.examAdminService.isScreenProctoringEnabled(runningExam); final boolean screenProctoringEnabled = this.examAdminService.isScreenProctoringEnabled(runningExam);
final Collection<RemoteProctoringRoom> proctoringData = (proctoringEnabled) final Collection<RemoteProctoringRoom> proctoringData = (proctoringEnabled)
? this.examProcotringRoomService ? this.examProctoringRoomService
.getProctoringCollectingRooms(examId) .getProctoringCollectingRooms(examId)
.onError(error -> log.error("Failed to get RemoteProctoringRoom for exam: {}", examId, error)) .onError(error -> log.error("Failed to get RemoteProctoringRoom for exam: {}", examId, error))
.getOr(Collections.emptyList()) .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.sebrestriction.details=SEB Restriction Details
sebserver.exam.action.createClientToStartExam=Export Exam Connection Configuration 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.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 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.type.VDI.tooltip=Exam type specified for Virtual Desktop Infrastructure
sebserver.exam.status.UP_COMING=Up Coming sebserver.exam.status.UP_COMING=Up Coming
sebserver.exam.status.TEST_RUN=Test Run
sebserver.exam.status.RUNNING=Running sebserver.exam.status.RUNNING=Running
sebserver.exam.status.FINISHED=Finished sebserver.exam.status.FINISHED=Finished
sebserver.exam.status.ARCHIVED=Archived sebserver.exam.status.ARCHIVED=Archived