Merge remote-tracking branch 'origin/dev-1.4' into development
This commit is contained in:
commit
0e6da2fe92
13 changed files with 394 additions and 86 deletions
|
@ -338,6 +338,9 @@ public class TableFilter<ROW extends ModelIdAware> {
|
||||||
TableFilter.this.entityTable.applyFilter();
|
TableFilter.this.entityTable.applyFilter();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.textInput.addListener(SWT.FocusOut, event -> {
|
||||||
|
TableFilter.this.entityTable.applyFilter();
|
||||||
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,17 +94,17 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
||||||
* @return Result refer to all exams for LMS update or to an error when happened */
|
* @return Result refer to all exams for LMS update or to an error when happened */
|
||||||
Result<Collection<Exam>> allForLMSUpdate();
|
Result<Collection<Exam>> allForLMSUpdate();
|
||||||
|
|
||||||
/** This is used to get all Exams to check if they have to set into running state in the meanwhile.
|
/** This is used to get all Exams that potentially needs a state change.
|
||||||
* Gets all exams in the upcoming status for run-check
|
* Checks if the stored running time frame of the exam is not in sync with the current state and return
|
||||||
|
* all exams for this is the case.
|
||||||
|
* Adding also leadTime before and followupTime after the specified running time frame of the exam for
|
||||||
|
* this check.
|
||||||
*
|
*
|
||||||
|
* @param leadTime Time period in milliseconds that is added to now-time-point to check the start time of the exam
|
||||||
|
* @param followupTime Time period in milliseconds that is subtracted from now-time-point check the end time of the
|
||||||
|
* exam
|
||||||
* @return Result refer to a collection of exams or to an error if happened */
|
* @return Result refer to a collection of exams or to an error if happened */
|
||||||
Result<Collection<Exam>> allForRunCheck();
|
Result<Collection<Exam>> allThatNeedsStatusUpdate(long leadTime, long followupTime);
|
||||||
|
|
||||||
/** This is used to get all Exams to check if they have to set into finished state in the meanwhile.
|
|
||||||
* Gets all exams in the running status for end-check
|
|
||||||
*
|
|
||||||
* @return Result refer to a collection of exams or to an error if happened */
|
|
||||||
Result<Collection<Exam>> allForEndCheck();
|
|
||||||
|
|
||||||
/** Get a collection of all currently running exam identifiers
|
/** Get a collection of all currently running exam identifiers
|
||||||
*
|
*
|
||||||
|
|
|
@ -317,16 +317,9 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Collection<Exam>> allForRunCheck() {
|
public Result<Collection<Exam>> allThatNeedsStatusUpdate(final long leadTime, final long followupTime) {
|
||||||
return this.examRecordDAO
|
return this.examRecordDAO
|
||||||
.allForRunCheck()
|
.allThatNeedsStatusUpdate(leadTime, followupTime)
|
||||||
.flatMap(this::toDomainModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result<Collection<Exam>> allForEndCheck() {
|
|
||||||
return this.examRecordDAO
|
|
||||||
.allForEndCheck()
|
|
||||||
.flatMap(this::toDomainModel);
|
.flatMap(this::toDomainModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -799,4 +792,5 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
return exam;
|
return exam;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,10 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
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.joda.time.DateTime;
|
||||||
|
import org.joda.time.DateTimeZone;
|
||||||
import org.mybatis.dynamic.sql.SqlBuilder;
|
import org.mybatis.dynamic.sql.SqlBuilder;
|
||||||
|
import org.mybatis.dynamic.sql.SqlCriterion;
|
||||||
import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter;
|
import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter;
|
||||||
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
|
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -435,9 +438,51 @@ public class ExamRecordDAO {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<ExamRecord>> allForRunCheck() {
|
public Result<Collection<ExamRecord>> allThatNeedsStatusUpdate(final long leadTime, final long followupTime) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
return this.examRecordMapper.selectByExample()
|
|
||||||
|
final DateTime now = DateTime.now(DateTimeZone.UTC);
|
||||||
|
final List<ExamRecord> result = new ArrayList<>();
|
||||||
|
|
||||||
|
// check those on running state that are not within the time-frame anymore
|
||||||
|
final List<ExamRecord> running = this.examRecordMapper.selectByExample()
|
||||||
|
.where(
|
||||||
|
ExamRecordDynamicSqlSupport.active,
|
||||||
|
isEqualTo(BooleanUtils.toInteger(true)))
|
||||||
|
.and(
|
||||||
|
ExamRecordDynamicSqlSupport.status,
|
||||||
|
isEqualTo(ExamStatus.RUNNING.name()))
|
||||||
|
.and(
|
||||||
|
ExamRecordDynamicSqlSupport.updating,
|
||||||
|
isEqualTo(BooleanUtils.toInteger(false)))
|
||||||
|
.and( // not within time frame
|
||||||
|
ExamRecordDynamicSqlSupport.quizStartTime,
|
||||||
|
SqlBuilder.isGreaterThanOrEqualToWhenPresent(now.plus(leadTime)),
|
||||||
|
or(
|
||||||
|
ExamRecordDynamicSqlSupport.quizEndTime,
|
||||||
|
SqlBuilder.isLessThanWhenPresent(now.minus(followupTime))))
|
||||||
|
.build()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
// check those in not running state (and not archived) and are within the time-frame or on wrong side of the time-frame
|
||||||
|
// if finished but up-coming
|
||||||
|
final SqlCriterion<String> finished = or(
|
||||||
|
ExamRecordDynamicSqlSupport.status,
|
||||||
|
isEqualTo(ExamStatus.FINISHED.name()),
|
||||||
|
and(
|
||||||
|
ExamRecordDynamicSqlSupport.quizStartTime,
|
||||||
|
SqlBuilder.isGreaterThanOrEqualToWhenPresent(now.plus(leadTime))));
|
||||||
|
|
||||||
|
// if up-coming but finished
|
||||||
|
final SqlCriterion<String> upcoming = or(
|
||||||
|
ExamRecordDynamicSqlSupport.status,
|
||||||
|
isEqualTo(ExamStatus.UP_COMING.name()),
|
||||||
|
and(
|
||||||
|
ExamRecordDynamicSqlSupport.quizEndTime,
|
||||||
|
SqlBuilder.isLessThanWhenPresent(now.minus(followupTime))),
|
||||||
|
finished);
|
||||||
|
|
||||||
|
final List<ExamRecord> notRunning = this.examRecordMapper.selectByExample()
|
||||||
.where(
|
.where(
|
||||||
ExamRecordDynamicSqlSupport.active,
|
ExamRecordDynamicSqlSupport.active,
|
||||||
isEqualTo(BooleanUtils.toInteger(true)))
|
isEqualTo(BooleanUtils.toInteger(true)))
|
||||||
|
@ -450,26 +495,19 @@ public class ExamRecordDAO {
|
||||||
.and(
|
.and(
|
||||||
ExamRecordDynamicSqlSupport.updating,
|
ExamRecordDynamicSqlSupport.updating,
|
||||||
isEqualTo(BooleanUtils.toInteger(false)))
|
isEqualTo(BooleanUtils.toInteger(false)))
|
||||||
|
.and( // within time frame
|
||||||
|
ExamRecordDynamicSqlSupport.quizStartTime,
|
||||||
|
SqlBuilder.isLessThanWhenPresent(now.plus(leadTime)),
|
||||||
|
and(
|
||||||
|
ExamRecordDynamicSqlSupport.quizEndTime,
|
||||||
|
SqlBuilder.isGreaterThanOrEqualToWhenPresent(now.minus(followupTime))),
|
||||||
|
upcoming)
|
||||||
.build()
|
.build()
|
||||||
.execute();
|
.execute();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
result.addAll(running);
|
||||||
public Result<Collection<ExamRecord>> allForEndCheck() {
|
result.addAll(notRunning);
|
||||||
return Result.tryCatch(() -> {
|
return result;
|
||||||
return this.examRecordMapper.selectByExample()
|
|
||||||
.where(
|
|
||||||
ExamRecordDynamicSqlSupport.active,
|
|
||||||
isEqualTo(BooleanUtils.toInteger(true)))
|
|
||||||
.and(
|
|
||||||
ExamRecordDynamicSqlSupport.status,
|
|
||||||
isEqualTo(ExamStatus.RUNNING.name()))
|
|
||||||
.and(
|
|
||||||
ExamRecordDynamicSqlSupport.updating,
|
|
||||||
isEqualTo(BooleanUtils.toInteger(false)))
|
|
||||||
.build()
|
|
||||||
.execute();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,8 +79,6 @@ public class OpenEdxCourseRestriction implements SEBRestrictionAPI {
|
||||||
// not accessible within OAuth2 authentication (just with user - authentication),
|
// not accessible within OAuth2 authentication (just with user - authentication),
|
||||||
// we can only check if the endpoint is available for now. This is checked
|
// we can only check if the endpoint is available for now. This is checked
|
||||||
// if there is no 404 response.
|
// if there is no 404 response.
|
||||||
// TODO: Ask eduNEXT to implement also OAuth2 API access for this endpoint to be able
|
|
||||||
// to check the version of the installed plugin.
|
|
||||||
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
|
final LmsSetup lmsSetup = this.openEdxRestTemplateFactory.apiTemplateDataSupplier.getLmsSetup();
|
||||||
final String url = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO;
|
final String url = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO;
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,26 @@ public class MockCourseAccessAPI implements CourseAccessAPI {
|
||||||
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
|
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
|
||||||
null,
|
null,
|
||||||
"http://lms.mockup.com/api/"));
|
"http://lms.mockup.com/api/"));
|
||||||
|
|
||||||
|
if (webserviceInfo.hasProfile("dev")) {
|
||||||
|
for (int i = 12; i < 50; i++) {
|
||||||
|
this.mockups.add(new QuizData(
|
||||||
|
"quiz10" + i, institutionId, lmsSetupId, lmsType, "Demo Quiz 10 " + i + " (MOCKUP)",
|
||||||
|
i + "_Starts in a minute and ends after five minutes",
|
||||||
|
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
|
||||||
|
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
|
||||||
|
DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS)
|
||||||
|
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
|
||||||
|
"http://lms.mockup.com/api/"));
|
||||||
|
this.mockups.add(new QuizData(
|
||||||
|
"quiz11" + i, institutionId, lmsSetupId, lmsType, "Demo Quiz 11 " + i + " (MOCKUP)",
|
||||||
|
i + "_Starts in a minute and ends never",
|
||||||
|
DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS)
|
||||||
|
.toString(Constants.DEFAULT_DATE_TIME_FORMAT),
|
||||||
|
null,
|
||||||
|
"http://lms.mockup.com/api/"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
public class ExamResetEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -2854284031889020212L;
|
||||||
|
|
||||||
|
public final Exam exam;
|
||||||
|
|
||||||
|
public ExamResetEvent(final Exam exam) {
|
||||||
|
super(exam);
|
||||||
|
this.exam = exam;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,7 +12,6 @@ import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
import org.joda.time.DateTimeZone;
|
import org.joda.time.DateTimeZone;
|
||||||
|
@ -107,8 +106,7 @@ public class ExamSessionControlTask implements DisposableBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
controlExamLMSUpdate();
|
controlExamLMSUpdate();
|
||||||
controlExamStart(updateId);
|
controlExamState(updateId);
|
||||||
controlExamEnd(updateId);
|
|
||||||
this.examDAO.releaseAgedLocks();
|
this.examDAO.releaseAgedLocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +189,7 @@ public class ExamSessionControlTask implements DisposableBean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void controlExamStart(final String updateId) {
|
private void controlExamState(final String updateId) {
|
||||||
if (log.isTraceEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
log.trace("Check starting exams: {}", updateId);
|
log.trace("Check starting exams: {}", updateId);
|
||||||
}
|
}
|
||||||
|
@ -199,45 +197,19 @@ public class ExamSessionControlTask implements DisposableBean {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final DateTime now = DateTime.now(DateTimeZone.UTC);
|
final DateTime now = DateTime.now(DateTimeZone.UTC);
|
||||||
final Map<Long, String> updated = this.examDAO.allForRunCheck()
|
this.examDAO
|
||||||
|
.allThatNeedsStatusUpdate(this.examTimePrefix, this.examTimeSuffix)
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(exam -> exam.startTime != null && exam.startTime.minus(this.examTimePrefix).isBefore(now))
|
.forEach(exam -> this.examUpdateHandler.updateState(
|
||||||
.filter(exam -> exam.endTime == null || exam.endTime.plus(this.examTimeSuffix).isAfter(now))
|
exam,
|
||||||
.flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setRunning(exam, updateId)))
|
now,
|
||||||
.collect(Collectors.toMap(Exam::getId, Exam::getName));
|
this.examTimePrefix,
|
||||||
|
this.examTimeSuffix,
|
||||||
if (!updated.isEmpty()) {
|
updateId));
|
||||||
log.info("Updated exams to running state: {}", updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Unexpected error while trying to update exams: ", e);
|
log.error("Unexpected error while trying to run exam state update task: ", e);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void controlExamEnd(final String updateId) {
|
|
||||||
if (log.isTraceEnabled()) {
|
|
||||||
log.trace("Check ending exams: {}", updateId);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
final DateTime now = DateTime.now(DateTimeZone.UTC);
|
|
||||||
|
|
||||||
final Map<Long, String> updated = this.examDAO.allForEndCheck()
|
|
||||||
.getOrThrow()
|
|
||||||
.stream()
|
|
||||||
.filter(exam -> exam.endTime != null && exam.endTime.plus(this.examTimeSuffix).isBefore(now))
|
|
||||||
.flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setFinished(exam, updateId)))
|
|
||||||
.collect(Collectors.toMap(Exam::getId, Exam::getName));
|
|
||||||
|
|
||||||
if (!updated.isEmpty()) {
|
|
||||||
log.info("Updated exams to finished state: {}", updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (final Exception e) {
|
|
||||||
log.error("Unexpected error while trying to update exams: ", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ 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.ExamFinishedEvent;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
|
@ -436,6 +437,24 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
||||||
.getAllActiveConnectionTokens(examId);
|
.getAllActiveConnectionTokens(examId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void notifyExamRest(final ExamResetEvent event) {
|
||||||
|
log.info("ExamResetEvent received, process exam session cleanup...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isExamRunning(event.exam.id)) {
|
||||||
|
this.flushCache(event.exam);
|
||||||
|
if (this.distributedSetup) {
|
||||||
|
this.clientConnectionDAO
|
||||||
|
.deleteClientIndicatorValues(event.exam)
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.error("Failed to cleanup on reset exam: {}", event.exam, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@EventListener
|
@EventListener
|
||||||
public void notifyExamFinished(final ExamFinishedEvent event) {
|
public void notifyExamFinished(final ExamFinishedEvent event) {
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||||
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.lms.impl.moodle.legacy.MoodleCourseAccess;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.legacy.MoodleCourseAccess;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamResetEvent;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
|
@ -89,6 +90,11 @@ class ExamUpdateHandler {
|
||||||
|
|
||||||
this.lmsAPIService
|
this.lmsAPIService
|
||||||
.getLmsAPITemplate(lmsSetupId)
|
.getLmsAPITemplate(lmsSetupId)
|
||||||
|
.map(template -> {
|
||||||
|
// TODO flush only involved courses from cache!
|
||||||
|
template.clearCourseCache();
|
||||||
|
return template;
|
||||||
|
})
|
||||||
.flatMap(template -> template.getQuizzes(new HashSet<>(exams.keySet())))
|
.flatMap(template -> template.getQuizzes(new HashSet<>(exams.keySet())))
|
||||||
.onError(error -> log.warn(
|
.onError(error -> log.warn(
|
||||||
"Failed to get quizzes from LMS Setup: {} cause: {}",
|
"Failed to get quizzes from LMS Setup: {} cause: {}",
|
||||||
|
@ -151,6 +157,113 @@ class ExamUpdateHandler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateState(
|
||||||
|
final Exam exam,
|
||||||
|
final DateTime now,
|
||||||
|
final long leadTime,
|
||||||
|
final long followupTime,
|
||||||
|
final String updateId) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Include leadTime and followupTime
|
||||||
|
final DateTime startTimeThreshold = now.plus(leadTime);
|
||||||
|
final DateTime endTimeThreshold = now.minus(leadTime);
|
||||||
|
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Check exam update for startTimeThreshold: {}, endTimeThreshold {}, exam: {}",
|
||||||
|
startTimeThreshold,
|
||||||
|
endTimeThreshold,
|
||||||
|
exam);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exam.status == ExamStatus.ARCHIVED) {
|
||||||
|
log.warn("Exam in unexpected state for status update. Skip update. Exam: {}", exam);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exam.status != ExamStatus.RUNNING && withinTimeframe(
|
||||||
|
exam.startTime,
|
||||||
|
startTimeThreshold,
|
||||||
|
exam.endTime,
|
||||||
|
endTimeThreshold)) {
|
||||||
|
|
||||||
|
if (withinTimeframe(exam.startTime, startTimeThreshold, exam.endTime, endTimeThreshold)) {
|
||||||
|
setRunning(exam, updateId)
|
||||||
|
.onError(error -> log.error("Failed to update exam to running state: {}",
|
||||||
|
exam,
|
||||||
|
error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exam.status != ExamStatus.FINISHED &&
|
||||||
|
exam.endTime != null &&
|
||||||
|
endTimeThreshold.isAfter(exam.endTime)) {
|
||||||
|
setFinished(exam, updateId)
|
||||||
|
.onError(error -> log.error("Failed to update exam to finished state: {}",
|
||||||
|
exam,
|
||||||
|
error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exam.status != ExamStatus.UP_COMING &&
|
||||||
|
exam.startTime != null &&
|
||||||
|
startTimeThreshold.isBefore(exam.startTime)) {
|
||||||
|
setUpcoming(exam, updateId)
|
||||||
|
.onError(error -> log.error("Failed to update exam to up-coming state: {}",
|
||||||
|
exam,
|
||||||
|
error));
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.error("Unexpected error while trying to update exam state for exam: {}", exam, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean withinTimeframe(
|
||||||
|
final DateTime startTime,
|
||||||
|
final DateTime startTimeThreshold,
|
||||||
|
final DateTime endTime,
|
||||||
|
final DateTime endTimeThreshold) {
|
||||||
|
|
||||||
|
if (startTime == null && endTime == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startTime == null && endTime.isAfter(endTimeThreshold)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endTime == null && startTime.isBefore(startTimeThreshold)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (startTime.isBefore(startTimeThreshold) && endTime.isAfter(endTimeThreshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Exam> setUpcoming(final Exam exam, final String updateId) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Update exam as up-coming: {}", exam);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.examDAO
|
||||||
|
.placeLock(exam.id, updateId)
|
||||||
|
.flatMap(e -> this.examDAO.updateState(exam.id, ExamStatus.UP_COMING, updateId))
|
||||||
|
.map(e -> {
|
||||||
|
this.examDAO
|
||||||
|
.releaseLock(e, updateId)
|
||||||
|
.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 ExamResetEvent(exam));
|
||||||
|
return exam;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Result<Exam> setRunning(final Exam exam, final String updateId) {
|
Result<Exam> setRunning(final Exam exam, final String updateId) {
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("Update exam as running: {}", exam);
|
log.debug("Update exam as running: {}", exam);
|
||||||
|
|
|
@ -137,10 +137,11 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
final HttpServletRequest request) {
|
final HttpServletRequest request) {
|
||||||
|
|
||||||
checkReadPrivilege(institutionId);
|
checkReadPrivilege(institutionId);
|
||||||
|
this.authorization.check(
|
||||||
|
PrivilegeType.READ,
|
||||||
|
EntityType.EXAM,
|
||||||
|
institutionId);
|
||||||
|
|
||||||
// NOTE: several attributes for sorting may be originated by the QuizData from LMS not by the database
|
|
||||||
// of the SEB Server. Therefore in the case we have no or the default sorting we can use the
|
|
||||||
// native PaginationService within MyBatis and SQL. For the other cases we need an in-line sorting and paging
|
|
||||||
if (StringUtils.isBlank(sort) ||
|
if (StringUtils.isBlank(sort) ||
|
||||||
(this.paginationService.isNativeSortingSupported(ExamRecordDynamicSqlSupport.examRecord, sort))) {
|
(this.paginationService.isNativeSortingSupported(ExamRecordDynamicSqlSupport.examRecord, sort))) {
|
||||||
|
|
||||||
|
@ -148,11 +149,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
this.authorization.check(
|
|
||||||
PrivilegeType.READ,
|
|
||||||
EntityType.EXAM,
|
|
||||||
institutionId);
|
|
||||||
|
|
||||||
final Collection<Exam> exams = this.examDAO
|
final Collection<Exam> exams = this.examDAO
|
||||||
.allMatching(new FilterMap(
|
.allMatching(new FilterMap(
|
||||||
allRequestParams,
|
allRequestParams,
|
||||||
|
|
|
@ -2393,9 +2393,21 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
||||||
assertNotNull(connectionPage);
|
assertNotNull(connectionPage);
|
||||||
assertFalse(connectionPage.isEmpty());
|
assertFalse(connectionPage.isEmpty());
|
||||||
|
|
||||||
|
connectionPageRes = restService
|
||||||
|
.getBuilder(GetClientConnectionPage.class)
|
||||||
|
.withQueryParam(ClientConnection.FILTER_ATTR_INFO, "")
|
||||||
|
.withQueryParam(Page.ATTR_SORT, Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID)
|
||||||
|
.call();
|
||||||
|
|
||||||
|
assertNotNull(connectionPageRes);
|
||||||
|
connectionPage = connectionPageRes.get();
|
||||||
|
assertNotNull(connectionPage);
|
||||||
|
assertFalse(connectionPage.isEmpty());
|
||||||
|
|
||||||
connectionPageRes = restService
|
connectionPageRes = restService
|
||||||
.getBuilder(GetClientConnectionPage.class)
|
.getBuilder(GetClientConnectionPage.class)
|
||||||
.withQueryParam(ClientConnection.FILTER_ATTR_INFO, "ghfhrthjrt")
|
.withQueryParam(ClientConnection.FILTER_ATTR_INFO, "ghfhrthjrt")
|
||||||
|
.withQueryParam(Page.ATTR_SORT, Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID)
|
||||||
.call();
|
.call();
|
||||||
|
|
||||||
assertNotNull(connectionPageRes);
|
assertNotNull(connectionPageRes);
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* 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.integration.api.admin;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.FixMethodOrder;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runners.MethodSorters;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.test.context.jdbc.Sql;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Orientation;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.TitleOrientation;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.UserServiceImpl;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.weblayer.api.OrientationController;
|
||||||
|
|
||||||
|
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
|
||||||
|
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||||
|
public class OrientationAPITest extends AdministrationAPIIntegrationTester {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrientationController orientationController;
|
||||||
|
@Autowired
|
||||||
|
private UserServiceImpl userServiceImpl;
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest mockRequest;
|
||||||
|
|
||||||
|
private final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void init() {
|
||||||
|
this.userServiceImpl.setAuthenticationIfAbsent(new SEBServerUser(
|
||||||
|
-1L,
|
||||||
|
new UserInfo("user1", 1L, null, "admin", null, null, null, true, null, null,
|
||||||
|
EnumSet.allOf(UserRole.class).stream().map(r -> r.name()).collect(Collectors.toSet())),
|
||||||
|
null));
|
||||||
|
Mockito.when(this.mockRequest.getQueryString()).thenReturn("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1)
|
||||||
|
public void test1_GetPage() {
|
||||||
|
final Page<Orientation> page = this.orientationController.getPage(
|
||||||
|
1L, 0, 100, null,
|
||||||
|
new LinkedMultiValueMap<String, String>(),
|
||||||
|
this.mockRequest);
|
||||||
|
|
||||||
|
assertNotNull(page);
|
||||||
|
assertFalse(page.content.isEmpty());
|
||||||
|
assertEquals("100", String.valueOf(page.content.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(5)
|
||||||
|
public void test5_CreateAndSaveAndDelete() {
|
||||||
|
this.params.clear();
|
||||||
|
this.params.add(Domain.ORIENTATION.ATTR_CONFIG_ATTRIBUTE_ID, "1");
|
||||||
|
this.params.add(Domain.ORIENTATION.ATTR_GROUP_ID, "testAttribute");
|
||||||
|
this.params.add(Domain.ORIENTATION.ATTR_HEIGHT, "1");
|
||||||
|
this.params.add(Domain.ORIENTATION.ATTR_TEMPLATE_ID, "0");
|
||||||
|
this.params.add(Domain.ORIENTATION.ATTR_TITLE, "LEFT");
|
||||||
|
this.params.add(Domain.ORIENTATION.ATTR_VIEW_ID, "1");
|
||||||
|
this.params.add(Domain.ORIENTATION.ATTR_WIDTH, "1");
|
||||||
|
this.params.add(Domain.ORIENTATION.ATTR_X_POSITION, "1");
|
||||||
|
this.params.add(Domain.ORIENTATION.ATTR_Y_POSITION, "1");
|
||||||
|
this.params.add(Domain.ORIENTATION.TYPE_NAME, "testAttribute");
|
||||||
|
|
||||||
|
final Orientation create = this.orientationController.create(
|
||||||
|
this.params,
|
||||||
|
1L,
|
||||||
|
this.mockRequest);
|
||||||
|
|
||||||
|
assertNotNull(create);
|
||||||
|
assertNotNull(create.id);
|
||||||
|
assertEquals("testAttribute", create.groupId);
|
||||||
|
assertEquals(1, create.height);
|
||||||
|
assertEquals(1, create.width);
|
||||||
|
assertEquals(1, create.xPosition);
|
||||||
|
assertEquals(1, create.yPosition);
|
||||||
|
assertEquals(TitleOrientation.LEFT, create.title);
|
||||||
|
|
||||||
|
final Orientation savePut = this.orientationController.savePut(new Orientation(
|
||||||
|
create.id,
|
||||||
|
null, null, null, null, null, null, null, null,
|
||||||
|
TitleOrientation.RIGHT));
|
||||||
|
|
||||||
|
assertNotNull(savePut);
|
||||||
|
assertNotNull(savePut.id);
|
||||||
|
assertEquals("testAttribute", savePut.groupId);
|
||||||
|
assertEquals(TitleOrientation.RIGHT, savePut.title);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue