From 147489b3b06e58251a731eaeda3e7c9772f18a11 Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 13 Jul 2022 10:35:49 +0200 Subject: [PATCH 1/4] more tests --- .../integration/UseCasesIntegrationTest.java | 12 ++ .../api/admin/OrientationAPITest.java | 117 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/OrientationAPITest.java diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index 53a3ff1b..67bb12ec 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -2393,9 +2393,21 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { assertNotNull(connectionPage); 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 .getBuilder(GetClientConnectionPage.class) .withQueryParam(ClientConnection.FILTER_ATTR_INFO, "ghfhrthjrt") + .withQueryParam(Page.ATTR_SORT, Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID) .call(); assertNotNull(connectionPageRes); diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/OrientationAPITest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/OrientationAPITest.java new file mode 100644 index 00000000..e41fcffc --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/OrientationAPITest.java @@ -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 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 page = this.orientationController.getPage( + 1L, 0, 100, null, + new LinkedMultiValueMap(), + 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); + + } + +} From 88ff9511f22fd039e4e09c440a7b979e7720e073 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 14 Jul 2022 10:21:56 +0200 Subject: [PATCH 2/4] SEBSERV-339 fixed all exam state changes and optimized code --- .../webservice/servicelayer/dao/ExamDAO.java | 18 +-- .../servicelayer/dao/impl/ExamDAOImpl.java | 12 +- .../servicelayer/dao/impl/ExamRecordDAO.java | 76 ++++++--- .../impl/edx/OpenEdxCourseRestriction.java | 2 - .../servicelayer/session/ExamResetEvent.java | 26 +++ .../session/impl/ExamSessionControlTask.java | 102 +++++++----- .../session/impl/ExamSessionServiceImpl.java | 19 +++ .../session/impl/ExamUpdateHandler.java | 149 ++++++++++++++++++ 8 files changed, 327 insertions(+), 77 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamResetEvent.java diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java index 7c6b77c9..429a8ddc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java @@ -94,17 +94,17 @@ public interface ExamDAO extends ActivatableEntityDAO, BulkActionSup * @return Result refer to all exams for LMS update or to an error when happened */ Result> allForLMSUpdate(); - /** This is used to get all Exams to check if they have to set into running state in the meanwhile. - * Gets all exams in the upcoming status for run-check + /** This is used to get all Exams that potentially needs a state change. + * 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 */ - Result> allForRunCheck(); - - /** 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> allForEndCheck(); + Result> allThatNeedsStatusUpdate(long leadTime, long followupTime); /** Get a collection of all currently running exam identifiers * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index 665e7b91..1049592e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -317,16 +317,9 @@ public class ExamDAOImpl implements ExamDAO { } @Override - public Result> allForRunCheck() { + public Result> allThatNeedsStatusUpdate(final long leadTime, final long followupTime) { return this.examRecordDAO - .allForRunCheck() - .flatMap(this::toDomainModel); - } - - @Override - public Result> allForEndCheck() { - return this.examRecordDAO - .allForEndCheck() + .allThatNeedsStatusUpdate(leadTime, followupTime) .flatMap(this::toDomainModel); } @@ -799,4 +792,5 @@ public class ExamDAOImpl implements ExamDAO { return exam; }); } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java index 5b2e8f10..a7aed3cb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java @@ -19,7 +19,10 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; 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.SqlCriterion; import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter; import org.mybatis.dynamic.sql.select.QueryExpressionDSL; import org.slf4j.Logger; @@ -435,9 +438,51 @@ public class ExamRecordDAO { } @Transactional(readOnly = true) - public Result> allForRunCheck() { + public Result> allThatNeedsStatusUpdate(final long leadTime, final long followupTime) { return Result.tryCatch(() -> { - return this.examRecordMapper.selectByExample() + + final DateTime now = DateTime.now(DateTimeZone.UTC); + final List result = new ArrayList<>(); + + // check those on running state that are not within the time-frame anymore + final List 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 finished = or( + ExamRecordDynamicSqlSupport.status, + isEqualTo(ExamStatus.FINISHED.name()), + and( + ExamRecordDynamicSqlSupport.quizStartTime, + SqlBuilder.isGreaterThanOrEqualToWhenPresent(now.plus(leadTime)))); + + // if up-coming but finished + final SqlCriterion upcoming = or( + ExamRecordDynamicSqlSupport.status, + isEqualTo(ExamStatus.UP_COMING.name()), + and( + ExamRecordDynamicSqlSupport.quizEndTime, + SqlBuilder.isLessThanWhenPresent(now.minus(followupTime))), + finished); + + final List notRunning = this.examRecordMapper.selectByExample() .where( ExamRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(true))) @@ -450,26 +495,19 @@ public class ExamRecordDAO { .and( ExamRecordDynamicSqlSupport.updating, 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() .execute(); - }); - } - @Transactional(readOnly = true) - public Result> allForEndCheck() { - return Result.tryCatch(() -> { - 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(); + result.addAll(running); + result.addAll(notRunning); + return result; }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java index fceb011e..b6c16975 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java @@ -79,8 +79,6 @@ public class OpenEdxCourseRestriction implements SEBRestrictionAPI { // not accessible within OAuth2 authentication (just with user - authentication), // we can only check if the endpoint is available for now. This is checked // 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 String url = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamResetEvent.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamResetEvent.java new file mode 100644 index 00000000..440c1f72 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamResetEvent.java @@ -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; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java index e028ef35..8b1375fc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java @@ -12,7 +12,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -107,8 +106,9 @@ public class ExamSessionControlTask implements DisposableBean { } controlExamLMSUpdate(); - controlExamStart(updateId); - controlExamEnd(updateId); + controlExamState(updateId); +// controlExamStart(updateId); +// controlExamEnd(updateId); this.examDAO.releaseAgedLocks(); } @@ -191,7 +191,7 @@ public class ExamSessionControlTask implements DisposableBean { } } - private void controlExamStart(final String updateId) { + private void controlExamState(final String updateId) { if (log.isTraceEnabled()) { log.trace("Check starting exams: {}", updateId); } @@ -199,47 +199,73 @@ public class ExamSessionControlTask implements DisposableBean { try { final DateTime now = DateTime.now(DateTimeZone.UTC); - final Map updated = this.examDAO.allForRunCheck() + this.examDAO + .allThatNeedsStatusUpdate(this.examTimePrefix, this.examTimeSuffix) .getOrThrow() .stream() - .filter(exam -> exam.startTime != null && exam.startTime.minus(this.examTimePrefix).isBefore(now)) - .filter(exam -> exam.endTime == null || exam.endTime.plus(this.examTimeSuffix).isAfter(now)) - .flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setRunning(exam, updateId))) - .collect(Collectors.toMap(Exam::getId, Exam::getName)); - - if (!updated.isEmpty()) { - log.info("Updated exams to running state: {}", updated); - } + .forEach(exam -> this.examUpdateHandler.updateState( + exam, + now, + this.examTimePrefix, + this.examTimeSuffix, + updateId)); } 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 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); - } - } +// @Deprecated +// private void controlExamStart(final String updateId) { +// if (log.isTraceEnabled()) { +// log.trace("Check starting exams: {}", updateId); +// } +// +// try { +// +// final DateTime now = DateTime.now(DateTimeZone.UTC); +// final Map updated = this.examDAO.allForRunCheck() +// .getOrThrow() +// .stream() +// .filter(exam -> exam.startTime != null && exam.startTime.minus(this.examTimePrefix).isBefore(now)) +// .filter(exam -> exam.endTime == null || exam.endTime.plus(this.examTimeSuffix).isAfter(now)) +// .flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setRunning(exam, updateId))) +// .collect(Collectors.toMap(Exam::getId, Exam::getName)); +// +// if (!updated.isEmpty()) { +// log.info("Updated exams to running state: {}", updated); +// } +// +// } catch (final Exception e) { +// log.error("Unexpected error while trying to update exams: ", e); +// } +// } +// +// @Deprecated +// 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 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); +// } +// } private void updateMaster() { this.webserviceInfo.updateMaster(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index 9144daeb..84be861e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -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.SEBRestrictionService; 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; @Lazy @@ -436,6 +437,24 @@ public class ExamSessionServiceImpl implements ExamSessionService { .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 public void notifyExamFinished(final ExamFinishedEvent event) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java index 83f1b9ba..093d7037 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java @@ -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.impl.moodle.legacy.MoodleCourseAccess; 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; @Lazy @@ -89,6 +90,10 @@ class ExamUpdateHandler { this.lmsAPIService .getLmsAPITemplate(lmsSetupId) + .map(template -> { + template.clearCourseCache(); + return template; + }) .flatMap(template -> template.getQuizzes(new HashSet<>(exams.keySet()))) .onError(error -> log.warn( "Failed to get quizzes from LMS Setup: {} cause: {}", @@ -151,6 +156,150 @@ 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)); + } + +// switch (exam.status) { +// case UP_COMING: { +// // move to RUNNING when now is within the running time frame +// if (withinTimeframe(exam.startTime, startTimeThreshold, exam.endTime, endTimeThreshold)) { +// setRunning(exam, updateId) +// .onError(error -> log.error("Failed to update exam to running state: {}", exam, error)); +// break; +// } +// // move to FINISHED when now is behind the end date +// if (exam.endTime != null && endTimeThreshold.isAfter(exam.endTime)) { +// setFinished(exam, updateId) +// .onError( +// error -> log.error("Failed to update exam to finished state: {}", exam, error)); +// break; +// } +// } +// case RUNNING: { +// // move to FINISHED when now is behind the end date +// if (exam.endTime != null && endTimeThreshold.isAfter(exam.endTime)) { +// setFinished(exam, updateId) +// .onError( +// error -> log.error("Failed to update exam to finished state: {}", exam, error)); +// break; +// } +// // move to UP_COMMING when now is before the start date +// break; +// } +// case FINISHED: { +// // move to RUNNING when now is within the running time frame +// // move to UP_COMMING when now is before the start date +// break; +// } +// default: { +// log.warn("Exam for status update in unexpected state. Skip update. Exam: {}", exam); +// } +// } + } 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 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 setRunning(final Exam exam, final String updateId) { if (log.isDebugEnabled()) { log.debug("Update exam as running: {}", exam); From 17c46362a352233e8aa1d3d14d8c74231e68dd57 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 14 Jul 2022 10:24:12 +0200 Subject: [PATCH 3/4] code cleanup --- .../session/impl/ExamSessionControlTask.java | 54 ------------------- .../session/impl/ExamUpdateHandler.java | 38 +------------ 2 files changed, 1 insertion(+), 91 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java index 8b1375fc..ac43a2e6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java @@ -107,8 +107,6 @@ public class ExamSessionControlTask implements DisposableBean { controlExamLMSUpdate(); controlExamState(updateId); -// controlExamStart(updateId); -// controlExamEnd(updateId); this.examDAO.releaseAgedLocks(); } @@ -215,58 +213,6 @@ public class ExamSessionControlTask implements DisposableBean { } } -// @Deprecated -// private void controlExamStart(final String updateId) { -// if (log.isTraceEnabled()) { -// log.trace("Check starting exams: {}", updateId); -// } -// -// try { -// -// final DateTime now = DateTime.now(DateTimeZone.UTC); -// final Map updated = this.examDAO.allForRunCheck() -// .getOrThrow() -// .stream() -// .filter(exam -> exam.startTime != null && exam.startTime.minus(this.examTimePrefix).isBefore(now)) -// .filter(exam -> exam.endTime == null || exam.endTime.plus(this.examTimeSuffix).isAfter(now)) -// .flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setRunning(exam, updateId))) -// .collect(Collectors.toMap(Exam::getId, Exam::getName)); -// -// if (!updated.isEmpty()) { -// log.info("Updated exams to running state: {}", updated); -// } -// -// } catch (final Exception e) { -// log.error("Unexpected error while trying to update exams: ", e); -// } -// } -// -// @Deprecated -// 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 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); -// } -// } - private void updateMaster() { this.webserviceInfo.updateMaster(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java index 093d7037..a3dd4a3c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java @@ -91,6 +91,7 @@ class ExamUpdateHandler { this.lmsAPIService .getLmsAPITemplate(lmsSetupId) .map(template -> { + // TODO flush only involved courses from cache! template.clearCourseCache(); return template; }) @@ -213,43 +214,6 @@ class ExamUpdateHandler { exam, error)); } - -// switch (exam.status) { -// case UP_COMING: { -// // move to RUNNING when now is within the running time frame -// if (withinTimeframe(exam.startTime, startTimeThreshold, exam.endTime, endTimeThreshold)) { -// setRunning(exam, updateId) -// .onError(error -> log.error("Failed to update exam to running state: {}", exam, error)); -// break; -// } -// // move to FINISHED when now is behind the end date -// if (exam.endTime != null && endTimeThreshold.isAfter(exam.endTime)) { -// setFinished(exam, updateId) -// .onError( -// error -> log.error("Failed to update exam to finished state: {}", exam, error)); -// break; -// } -// } -// case RUNNING: { -// // move to FINISHED when now is behind the end date -// if (exam.endTime != null && endTimeThreshold.isAfter(exam.endTime)) { -// setFinished(exam, updateId) -// .onError( -// error -> log.error("Failed to update exam to finished state: {}", exam, error)); -// break; -// } -// // move to UP_COMMING when now is before the start date -// break; -// } -// case FINISHED: { -// // move to RUNNING when now is within the running time frame -// // move to UP_COMMING when now is before the start date -// break; -// } -// default: { -// log.warn("Exam for status update in unexpected state. Skip update. Exam: {}", exam); -// } -// } } catch (final Exception e) { log.error("Unexpected error while trying to update exam state for exam: {}", exam, e); } From 921595c3b07c038a3c6ccf518210131dd1c9d548 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 14 Jul 2022 13:05:05 +0200 Subject: [PATCH 4/4] added focus out for auto-filter on tex filter for lists --- .../seb/sebserver/gui/table/TableFilter.java | 3 +++ .../lms/impl/mockup/MockCourseAccessAPI.java | 20 +++++++++++++++++++ .../api/ExamAdministrationController.java | 12 ++++------- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java index 7d51dc1c..6a9646c5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/TableFilter.java @@ -338,6 +338,9 @@ public class TableFilter { TableFilter.this.entityTable.applyFilter(); } }); + this.textInput.addListener(SWT.FocusOut, event -> { + TableFilter.this.entityTable.applyFilter(); + }); return this; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java index 537f2ceb..a92c65a7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java @@ -91,6 +91,26 @@ public class MockCourseAccessAPI implements CourseAccessAPI { .toString(Constants.DEFAULT_DATE_TIME_FORMAT), null, "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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 32bdfac6..e87b585b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -137,10 +137,11 @@ public class ExamAdministrationController extends EntityController { final HttpServletRequest request) { 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) || (this.paginationService.isNativeSortingSupported(ExamRecordDynamicSqlSupport.examRecord, sort))) { @@ -148,11 +149,6 @@ public class ExamAdministrationController extends EntityController { } else { - this.authorization.check( - PrivilegeType.READ, - EntityType.EXAM, - institutionId); - final Collection exams = this.examDAO .allMatching(new FilterMap( allRequestParams,