implemented event handling for start and finish exams
This commit is contained in:
parent
b6433c7c99
commit
ebbbf56314
8 changed files with 140 additions and 37 deletions
|
@ -22,6 +22,7 @@ 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.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@ -38,6 +39,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
@Service
|
@Service
|
||||||
|
@ -188,6 +191,30 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
|
||||||
.flatMap(this.examDAO::byPK);
|
.flatMap(this.examDAO::byPK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void notifyExamStarted(final ExamStartedEvent event) {
|
||||||
|
|
||||||
|
log.info("ExamStartedEvent received, process applySEBClientRestriction...");
|
||||||
|
|
||||||
|
applySEBClientRestriction(event.exam)
|
||||||
|
.onError(error -> log.error(
|
||||||
|
"Failed to apply SEB restrictions for started exam: {}",
|
||||||
|
event.exam,
|
||||||
|
error));
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void notifyExamFinished(final ExamFinishedEvent event) {
|
||||||
|
|
||||||
|
log.info("ExamFinishedEvent received, process releaseSEBClientRestriction...");
|
||||||
|
|
||||||
|
releaseSEBClientRestriction(event.exam)
|
||||||
|
.onError(error -> log.error(
|
||||||
|
"Failed to release SEB restrictions for finished exam: {}",
|
||||||
|
event.exam,
|
||||||
|
error));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Exam> applySEBClientRestriction(final Exam exam) {
|
public Result<Exam> applySEBClientRestriction(final Exam exam) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
|
|
||||||
|
/** This event is fired just after an exam has been finished */
|
||||||
|
public class ExamFinishedEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -1528880878532843063L;
|
||||||
|
|
||||||
|
public final Exam exam;
|
||||||
|
|
||||||
|
public ExamFinishedEvent(final Exam exam) {
|
||||||
|
super(exam);
|
||||||
|
this.exam = exam;
|
||||||
|
}
|
||||||
|
}
|
|
@ -191,13 +191,6 @@ public interface ExamSessionService {
|
||||||
* @return Result refer to the collection of connection tokens or to an error when happened. */
|
* @return Result refer to the collection of connection tokens or to an error when happened. */
|
||||||
Result<Collection<String>> getActiveConnectionTokens(Long examId);
|
Result<Collection<String>> getActiveConnectionTokens(Long examId);
|
||||||
|
|
||||||
/** Called to notify that the given exam has just been finished.
|
|
||||||
* This cleanup all exam session caches for the given exam and also cleanup session based stores on the persistent.
|
|
||||||
*
|
|
||||||
* @param exam the Exam that has just been finished
|
|
||||||
* @return Result refer to the finished exam or to an error when happened. */
|
|
||||||
Result<Exam> notifyExamFinished(final Exam exam);
|
|
||||||
|
|
||||||
/** Use this to check if the current cached running exam is up to date
|
/** Use this to check if the current cached running exam is up to date
|
||||||
* and if not to flush the cache.
|
* and if not to flush the cache.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
|
|
||||||
|
/** This event is fired just after an exam has been started */
|
||||||
|
public class ExamStartedEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -6564345490588661010L;
|
||||||
|
|
||||||
|
public final Exam exam;
|
||||||
|
|
||||||
|
public ExamStartedEvent(final Exam exam) {
|
||||||
|
super(exam);
|
||||||
|
this.exam = exam;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -29,7 +29,6 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -43,7 +42,6 @@ public class ExamSessionControlTask implements DisposableBean {
|
||||||
private final ExamUpdateHandler examUpdateHandler;
|
private final ExamUpdateHandler examUpdateHandler;
|
||||||
private final ExamProctoringRoomService examProcotringRoomService;
|
private final ExamProctoringRoomService examProcotringRoomService;
|
||||||
private final WebserviceInfo webserviceInfo;
|
private final WebserviceInfo webserviceInfo;
|
||||||
private final ExamSessionService examSessionService;
|
|
||||||
|
|
||||||
private final Long examTimePrefix;
|
private final Long examTimePrefix;
|
||||||
private final Long examTimeSuffix;
|
private final Long examTimeSuffix;
|
||||||
|
@ -56,7 +54,6 @@ public class ExamSessionControlTask implements DisposableBean {
|
||||||
final ExamUpdateHandler examUpdateHandler,
|
final ExamUpdateHandler examUpdateHandler,
|
||||||
final ExamProctoringRoomService examProcotringRoomService,
|
final ExamProctoringRoomService examProcotringRoomService,
|
||||||
final WebserviceInfo webserviceInfo,
|
final WebserviceInfo webserviceInfo,
|
||||||
final ExamSessionService examSessionService,
|
|
||||||
@Value("${sebserver.webservice.api.exam.time-prefix:3600000}") final Long examTimePrefix,
|
@Value("${sebserver.webservice.api.exam.time-prefix:3600000}") final Long examTimePrefix,
|
||||||
@Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix,
|
@Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix,
|
||||||
@Value("${sebserver.webservice.api.exam.update-interval:1 * * * * *}") final String examTaskCron,
|
@Value("${sebserver.webservice.api.exam.update-interval:1 * * * * *}") final String examTaskCron,
|
||||||
|
@ -66,7 +63,6 @@ public class ExamSessionControlTask implements DisposableBean {
|
||||||
this.sebClientConnectionService = sebClientConnectionService;
|
this.sebClientConnectionService = sebClientConnectionService;
|
||||||
this.examUpdateHandler = examUpdateHandler;
|
this.examUpdateHandler = examUpdateHandler;
|
||||||
this.webserviceInfo = webserviceInfo;
|
this.webserviceInfo = webserviceInfo;
|
||||||
this.examSessionService = examSessionService;
|
|
||||||
this.examTimePrefix = examTimePrefix;
|
this.examTimePrefix = examTimePrefix;
|
||||||
this.examTimeSuffix = examTimeSuffix;
|
this.examTimeSuffix = examTimeSuffix;
|
||||||
this.examTaskCron = examTaskCron;
|
this.examTaskCron = examTaskCron;
|
||||||
|
@ -188,8 +184,6 @@ public class ExamSessionControlTask implements DisposableBean {
|
||||||
.stream()
|
.stream()
|
||||||
.filter(exam -> exam.endTime != null && exam.endTime.plus(this.examTimeSuffix).isBefore(now))
|
.filter(exam -> exam.endTime != null && exam.endTime.plus(this.examTimeSuffix).isBefore(now))
|
||||||
.flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setFinished(exam, updateId)))
|
.flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setFinished(exam, updateId)))
|
||||||
.flatMap(exam -> Result.skipOnError(this.examProcotringRoomService.disposeRoomsForExam(exam)))
|
|
||||||
.flatMap(exam -> Result.skipOnError(this.examSessionService.notifyExamFinished(exam)))
|
|
||||||
.collect(Collectors.toMap(Exam::getId, Exam::getName));
|
.collect(Collectors.toMap(Exam::getId, Exam::getName));
|
||||||
|
|
||||||
if (!updated.isEmpty()) {
|
if (!updated.isEmpty()) {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.cache.CacheManager;
|
import org.springframework.cache.CacheManager;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
|
@ -402,20 +404,23 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
||||||
.getActiveConnctionTokens(examId);
|
.getActiveConnctionTokens(examId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@EventListener
|
||||||
public Result<Exam> notifyExamFinished(final Exam exam) {
|
public void notifyExamFinished(final ExamFinishedEvent event) {
|
||||||
return Result.tryCatch(() -> {
|
|
||||||
if (!isExamRunning(exam.id)) {
|
log.info("ExamFinishedEvent received, process exam session cleanup...");
|
||||||
this.flushCache(exam);
|
|
||||||
|
try {
|
||||||
|
if (!isExamRunning(event.exam.id)) {
|
||||||
|
this.flushCache(event.exam);
|
||||||
if (this.distributedSetup) {
|
if (this.distributedSetup) {
|
||||||
this.clientConnectionDAO
|
this.clientConnectionDAO
|
||||||
.deleteClientIndicatorValues(exam)
|
.deleteClientIndicatorValues(event.exam)
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
return exam;
|
log.error("Failed to cleanup on finished exam: {}", event.exam, e);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.joda.time.DateTimeZone;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@ -24,6 +25,8 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
@Service
|
@Service
|
||||||
|
@ -33,17 +36,20 @@ class ExamUpdateHandler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(ExamUpdateHandler.class);
|
private static final Logger log = LoggerFactory.getLogger(ExamUpdateHandler.class);
|
||||||
|
|
||||||
private final ExamDAO examDAO;
|
private final ExamDAO examDAO;
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
private final SEBRestrictionService sebRestrictionService;
|
private final SEBRestrictionService sebRestrictionService;
|
||||||
private final String updatePrefix;
|
private final String updatePrefix;
|
||||||
private final Long examTimeSuffix;
|
private final Long examTimeSuffix;
|
||||||
|
|
||||||
public ExamUpdateHandler(
|
public ExamUpdateHandler(
|
||||||
final ExamDAO examDAO,
|
final ExamDAO examDAO,
|
||||||
|
final ApplicationEventPublisher applicationEventPublisher,
|
||||||
final SEBRestrictionService sebRestrictionService,
|
final SEBRestrictionService sebRestrictionService,
|
||||||
final WebserviceInfo webserviceInfo,
|
final WebserviceInfo webserviceInfo,
|
||||||
@Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix) {
|
@Value("${sebserver.webservice.api.exam.time-suffix:3600000}") final Long examTimeSuffix) {
|
||||||
|
|
||||||
this.examDAO = examDAO;
|
this.examDAO = examDAO;
|
||||||
|
this.applicationEventPublisher = applicationEventPublisher;
|
||||||
this.sebRestrictionService = sebRestrictionService;
|
this.sebRestrictionService = sebRestrictionService;
|
||||||
this.updatePrefix = webserviceInfo.getLocalHostAddress()
|
this.updatePrefix = webserviceInfo.getLocalHostAddress()
|
||||||
+ "_" + webserviceInfo.getServerPort() + "_";
|
+ "_" + webserviceInfo.getServerPort() + "_";
|
||||||
|
@ -79,14 +85,21 @@ class ExamUpdateHandler {
|
||||||
|
|
||||||
return this.examDAO
|
return this.examDAO
|
||||||
.placeLock(exam.id, updateId)
|
.placeLock(exam.id, updateId)
|
||||||
.flatMap(e -> this.examDAO.updateState(
|
.flatMap(e -> this.examDAO.updateState(exam.id, ExamStatus.RUNNING, updateId))
|
||||||
exam.id,
|
.map(e -> {
|
||||||
ExamStatus.RUNNING,
|
this.examDAO
|
||||||
updateId))
|
.releaseLock(e, updateId)
|
||||||
.flatMap(this.sebRestrictionService::applySEBClientRestriction)
|
.onError(error -> this.examDAO
|
||||||
.flatMap(e -> this.examDAO.releaseLock(e, updateId))
|
.forceUnlock(exam.id)
|
||||||
.onError(error -> this.examDAO.forceUnlock(exam.id)
|
.onError(unlockError -> log.error(
|
||||||
.onError(unlockError -> log.error("Failed to force unlock update look for exam: {}", exam.id)));
|
"Failed to force unlock update look for exam: {}",
|
||||||
|
exam.id)));
|
||||||
|
return e;
|
||||||
|
})
|
||||||
|
.map(e -> {
|
||||||
|
this.applicationEventPublisher.publishEvent(new ExamStartedEvent(exam));
|
||||||
|
return exam;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<Exam> setFinished(final Exam exam, final String updateId) {
|
Result<Exam> setFinished(final Exam exam, final String updateId) {
|
||||||
|
@ -96,13 +109,21 @@ class ExamUpdateHandler {
|
||||||
|
|
||||||
return this.examDAO
|
return this.examDAO
|
||||||
.placeLock(exam.id, updateId)
|
.placeLock(exam.id, updateId)
|
||||||
.flatMap(e -> this.examDAO.updateState(
|
.flatMap(e -> this.examDAO.updateState(exam.id, ExamStatus.FINISHED, updateId))
|
||||||
exam.id,
|
.map(e -> {
|
||||||
ExamStatus.FINISHED,
|
this.examDAO
|
||||||
updateId))
|
.releaseLock(e, updateId)
|
||||||
.flatMap(this.sebRestrictionService::releaseSEBClientRestriction)
|
.onError(error -> this.examDAO
|
||||||
.flatMap(e -> this.examDAO.releaseLock(e, updateId))
|
.forceUnlock(exam.id)
|
||||||
.onError(error -> this.examDAO.forceUnlock(exam.id));
|
.onError(unlockError -> log.error(
|
||||||
|
"Failed to force unlock update look for exam: {}",
|
||||||
|
exam.id)));
|
||||||
|
return e;
|
||||||
|
})
|
||||||
|
.map(e -> {
|
||||||
|
this.applicationEventPublisher.publishEvent(new ExamFinishedEvent(exam));
|
||||||
|
return exam;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
|
||||||
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.session.ExamFinishedEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||||
|
@ -155,6 +156,15 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void notifyExamFinished(final ExamFinishedEvent event) {
|
||||||
|
|
||||||
|
log.info("ExamFinishedEvent received, process disposeRoomsForExam...");
|
||||||
|
|
||||||
|
disposeRoomsForExam(event.exam)
|
||||||
|
.onError(error -> log.error("Failed to dispose rooms for finished exam: {}", event.exam, error));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Exam> disposeRoomsForExam(final Exam exam) {
|
public Result<Exam> disposeRoomsForExam(final Exam exam) {
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue