diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b1f4811132f46eb429eea5c4e4f1387629c284d4..08f9c0dcbe414bd245fb2ec5fd5b691f72da04f8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,9 +34,6 @@ variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "/certs" - SAST_DISABLE_DIND: "false" - SAST_DEFAULT_ANALYZERS: "spotbugs" - # Configure mysql environment variables MYSQL_DATABASE: "$DB_NAME" MYSQL_USER: "$DB_NAME" @@ -116,7 +113,9 @@ gradle_test: - codecov/ reports: junit: build/test-results/test/TEST-*.xml - cobertura: build/reports/jacoco/test/jacocoTestReport.xml + coverage_report: + coverage_format: cobertura + path: build/reports/jacoco/test/jacocoTestReport.xml before_script: - mv src/test/resources/application-h2.properties src/test/resources/application-test.properties script: @@ -406,23 +405,6 @@ spotbugs-sast: dependencies: - gradle_build -eslint-sast: - allow_failure: true - variables: - SAST_EXCLUDED_PATHS: tmp, build, target, out, .gradle, gradle, docs, codecov - rules: - - if: $CI_PIPELINE_SOURCE == "trigger" || - $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" - when: never - - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "development" || - $CI_MERGE_REQUEST_ID - stage: gitlab reports - needs: - - gradle_build - dependencies: - - gradle_build - # Run the DAST security checks and reporter. # Currently set to manual as it requires a test environment to be up and running. dast: diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a837d317ce16a4e5835f5963e0dfb85234ceca..d4e0e7cb134823105cd2ee205b420f6474ec33ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + +### Changed + +### Fixed + +### Deprecated + +### Removed + +## [2.1.0] +### Added - Redirect users to enrol page when they are not correctly enrolled for a lab. [@cedricwilleken](https://gitlab.ewi.tudelft.nl/cedricwilleken) - A Student Group gets created automatically when a student attempts to enqueue without one. [@lemaire](https://gitlab.ewi.tudelft.nl/Lemaire) - Allow managers and up to pick any request [@cedricwilleken](https://gitlab.ewi.tudelft.nl/cedricwilleken) @@ -30,12 +41,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add indicator for active filters [@cedricwillekens](https://gitlab.ewi.tudelft.nl/cedricwilleken) - Allow users to view and create new shared editions. [@cedricwillekens](https://gitlab.ewi.tudelft.nl/cedricwilleken) - Add room images for student requests [@cedricwilleken](https://gitlab.ewi.tudelft.nl/cedricwilleken) +- Hybrid Labs are now supported regardless of what the direction of the lab is. [@hpage](https://gitlab.ewi.tudelft.nl/hpage) +- Added buttons to edit sessions for a shared lab [@hpage](https://gitlab.ewi.tudelft.nl/hpage) ### Changed - Redirect students to their request when their are being processed when accessing the lab page. [@cedricwilleken](https://gitlab.ewi.tudelft.nl/cedricwilleken) - Provide a clearer error message to users when username cannot be found [@cedricwilleken](https://gitlab.ewi.tudelft.nl/cedricwilleken) - Also allow for constraints checking to be done on individual students. [@cedricwilleken](https://gitlab.ewi.tudelft.nl/cedricwilleken) - Finished course editions are now primarily ordered by the end date (DESC) and secondarily ordered by the role of the user. [@hpage](https://gitlab.ewi.tudelft.nl/hpage) +- Lab export now includes TA comments. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) ### Fixed - In exam lab, fix all requests getting rejected [@rbackx](https://gitlab.ewi.tudelft.nl/rbackx) @@ -46,6 +60,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix language of calendar [@cchen9](https://gitlab.ewi.tudelft.nl/cchen9) - Prevent error on returning to catalog or enrol page after enrolling in a course edition [@cchen9](https://gitlab.ewi.tudelft.nl/cchen9) - Form submission buttons on requests are disabled after they are pressed, preventing 2 requests to be sent. [@hpage](https://gitlab.ewi.tudelft.nl/hpage) +- Feedback is now deleted when the associated request is deleted [@hpage](https://gitlab.ewi.tudelft.nl/hpage) +- Assistants can no longer see requests belonging to other editions in a shared session [@hpage](https://gitlab.ewi.tudelft.nl/hpage) ## [1.2.0] - [2020-06-06] ### Added @@ -97,6 +113,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Get Next button will only show up if the lab is open. [@tnulle](https://gitlab.ewi.tudelft.nl/tnulle) - Improve performance of counting the number of requests current in the queue for a specific assistant and specific lab by [@lemaire](https://gitlab.ewi.tudelft.nl/lemaire) - Fix wrong submission link by remove email suffix [@tnulle](https://gitlab.ewi.tudelft.nl/tnulle) +- Requests for shared sessions are now only sent to the appropriate assistants. [@hpage](https://gitlab.ewi.tudelft.nl/hpage) ## [1.1.0] - (2019-12-16) ### Deprecated diff --git a/build.gradle.kts b/build.gradle.kts index 4c479e9ecad7ab912cb27f79bb2d47c60427ae4c..07a19081be111bc28d63f187d14d7855ae814bf4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,12 +4,12 @@ import com.diffplug.gradle.spotless.SpotlessExtension import org.springframework.boot.gradle.tasks.run.BootRun group = "nl.tudelft.ewi.queue" -version = "2.0.0" +version = "2.1.0" val javaVersion = JavaVersion.VERSION_17 val libradorVersion = "1.0.3-SNAPSHOT6" -val labradoorVersion = "1.3.1-SNAPSHOT" +val labradoorVersion = "1.3.5" val queryDslVersion = "4.4.0" // A definition of all dependencies and repositories where to find them that need to @@ -55,7 +55,7 @@ plugins { // Spring plugins for managing dependencies and creating // a nice Spring Boot application. - id("org.springframework.boot").version("2.5.12") + id("org.springframework.boot").version("2.5.14") id("io.spring.dependency-management").version("1.0.11.RELEASE") // Plugin to provide task to check the current versions of diff --git a/src/main/java/nl/tudelft/queue/QueueMvcConfig.java b/src/main/java/nl/tudelft/queue/QueueMvcConfig.java index 24804baf7581ab502c1d87e5cd1d7ce02ec5d462..6c7587bb0ca5bfb9173ff29a93919ef912c62e20 100644 --- a/src/main/java/nl/tudelft/queue/QueueMvcConfig.java +++ b/src/main/java/nl/tudelft/queue/QueueMvcConfig.java @@ -42,7 +42,7 @@ public class QueueMvcConfig implements WebMvcConfigurer { registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/"); registry.addResourceHandler("/img/**").addResourceLocations("classpath:/static/img/"); registry.addResourceHandler("/webjars/**").addResourceLocations("/webjars/").resourceChain(false); - registry.addResourceHandler("/**").addResourceLocations("/static/"); + registry.addResourceHandler("/**").addResourceLocations("classpath:/static/"); } } diff --git a/src/main/java/nl/tudelft/queue/controller/AdminController.java b/src/main/java/nl/tudelft/queue/controller/AdminController.java index 01dd038f8f30deea5ebb04f26b15696fa2c338a8..ae26344f00e4d0ad1cc30838894d790963027463 100644 --- a/src/main/java/nl/tudelft/queue/controller/AdminController.java +++ b/src/main/java/nl/tudelft/queue/controller/AdminController.java @@ -19,7 +19,10 @@ package nl.tudelft.queue.controller; import java.io.IOException; import java.time.LocalDateTime; +import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import javax.validation.Valid; @@ -30,13 +33,16 @@ import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.api.dto.CourseCreateDTO; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.librador.dto.view.View; import nl.tudelft.queue.cache.EditionCacheManager; import nl.tudelft.queue.cache.RoomCacheManager; import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.dto.create.QueueCourseCreateDTO; +import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO; import nl.tudelft.queue.model.labs.Lab; import nl.tudelft.queue.repository.LabRepository; import nl.tudelft.queue.service.AdminService; +import nl.tudelft.queue.service.EditionService; import nl.tudelft.queue.service.LabService; import org.springframework.beans.factory.annotation.Autowired; @@ -66,9 +72,10 @@ public class AdminController { @Autowired private EditionCacheManager eCache; - @Autowired - private LabRepository labRepository; + private EditionService es; + @Autowired + private LabRepository lr; @Autowired @Lazy @@ -112,20 +119,47 @@ public class AdminController { */ @GetMapping("/calendar") public String getAdminCoursesPage(Model model) { - Period currPeriod = new Period().start(LocalDateTime.now().minusDays(1)) + Period currPeriod = new Period().start(LocalDateTime.now().minusWeeks(1)) .end(LocalDateTime.now().plusMonths(1)); List<Lab> labs = ls.getAllLabsWithinPeriod(currPeriod); model.addAttribute("sessions", ls.convertToCalendarEntries(labs)); - long count; - if ((count = ls.countOngoingLabs(labs)) > 0) { - model.addAttribute("labsOngoing", String.format("Currently %d labs " + - "ongoing!", count)); + return "admin/view/calendar"; + } + + @GetMapping("/running") + public String getRunningLabs(Model model) { + LocalDateTime now = LocalDateTime.now(); + var runningEditions = eCache.get(Objects.requireNonNull( + eApi.getAllEditionsActiveAtDate(LocalDateTime.now()) + .map(EditionSummaryDTO::getId).collectList().block())); + + var runningSessions = sCache.get(runningEditions.stream() + .flatMap(e -> e.getSessions().stream()) + .filter(s -> s.getEnd().isAfter(now)) + .map(SessionSummaryDTO::getId)); + var runningLabs = es.sortLabs(lr + .findAllBySessions(runningSessions.stream().map(SessionDetailsDTO::getId) + .collect(Collectors.toList())) + .stream().map(l -> View.convert(l, QueueSessionSummaryDTO.class)) + .filter(qs -> qs.getSlot().today()) + .sorted(Comparator.comparing(qs -> qs.getSlot().getClosesAt())) + .collect(Collectors.toList())); + model.addAttribute("runningLabs", runningLabs); + + Period today = new Period().start(LocalDateTime.now().minusDays(1)) + .end(LocalDateTime.now().plusDays(1)); + List<Lab> labs = ls.getAllLabsWithinPeriod(today); + long labCount = ls.countOngoingLabs(labs); + if (labCount > 0) { + model.addAttribute("labsOngoing", String.format("Currently %d lab(s) " + + "ongoing", labCount)); } - if ((count = ls.countLabsWithOpenSlotSelection(labs)) > 0) { - model.addAttribute("slotSelectionOpen", String.format("Currently %d labs with slot " + - "selection open", count)); + long slotCount = ls.countLabsWithOpenSlotSelection(labs); + if (slotCount > 0) { + model.addAttribute("slotSelectionOpen", String.format("Currently %d lab(s) with slot " + + "selection open", slotCount)); } - return "admin/view/calendar"; + return "admin/view/running"; } /** diff --git a/src/main/java/nl/tudelft/queue/controller/EditionController.java b/src/main/java/nl/tudelft/queue/controller/EditionController.java index 27e431c762304cc02c38c0867fb4707c6accdb63..215c8011024668c2498bbdc6b98961949562cc12 100644 --- a/src/main/java/nl/tudelft/queue/controller/EditionController.java +++ b/src/main/java/nl/tudelft/queue/controller/EditionController.java @@ -18,12 +18,16 @@ package nl.tudelft.queue.controller; import static java.time.LocalDateTime.now; +import static nl.tudelft.labracore.api.dto.PersonDetailsDTO.DefaultRoleEnum.ADMIN; +import static nl.tudelft.labracore.api.dto.PersonDetailsDTO.DefaultRoleEnum.TEACHER; import static nl.tudelft.labracore.lib.LabracoreApiUtil.fromPageable; import static nl.tudelft.queue.PageUtil.toPage; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -153,6 +157,9 @@ public class EditionController { public String getEditionList(@AuthenticatedPerson Person person, @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable, Model model) { + + PersonDetailsDTO pd = Objects.requireNonNull(pApi.getPersonById(person.getId()).block()); + var filter = es.getFilter("/editions"); var editions = eApi .getEditionsPageActiveOrTaughtBy(person.getId(), fromPageable(pageable), filter.getPrograms(), @@ -171,6 +178,17 @@ public class EditionController { model.addAttribute("page", "catalog"); + if (pd.getDefaultRole() == ADMIN || pd.getDefaultRole() == TEACHER) { + model.addAttribute("allEditions", eCache.get( + Objects.requireNonNullElse( + eApi.getAllEditionsActiveDuringPeriod( + new Period().start(LocalDateTime.now()) + .end(LocalDateTime.now().plusYears(1))) + .map(EditionSummaryDTO::getId) + .collectList().block(), + List.of()))); + } + return "edition/index"; } diff --git a/src/main/java/nl/tudelft/queue/controller/HomeController.java b/src/main/java/nl/tudelft/queue/controller/HomeController.java index 207c71a074e565b4b9bf4715d175a7f6427057d3..75fbbea2289192d5699cd94b7fc159a8cb09a386 100644 --- a/src/main/java/nl/tudelft/queue/controller/HomeController.java +++ b/src/main/java/nl/tudelft/queue/controller/HomeController.java @@ -17,8 +17,6 @@ */ package nl.tudelft.queue.controller; -import static nl.tudelft.labracore.api.dto.PersonDetailsDTO.DefaultRoleEnum.ADMIN; -import static nl.tudelft.labracore.api.dto.PersonDetailsDTO.DefaultRoleEnum.TEACHER; import static nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO.TypeEnum.TEACHER_RO; import java.time.LocalDateTime; @@ -199,36 +197,6 @@ public class HomeController { .filter(qs -> ps.canEnqueueSelf(qs.getId()) || qs.getSlot().open()) .collect(Collectors.toList()))); - if (pd.getDefaultRole() == ADMIN) { - var runningEditions = eCache.get(Objects.requireNonNull( - eApi.getAllEditionsActiveAtDate(LocalDateTime.now()) - .map(EditionSummaryDTO::getId).collectList().block())); - - var runningSessions = sCache.get(runningEditions.stream() - .flatMap(e -> e.getSessions().stream()) - .filter(s -> s.getEnd().isAfter(now)) - .map(SessionSummaryDTO::getId)); - var runningLabs = es.sortLabs(lr - .findAllBySessions(runningSessions.stream().map(SessionDetailsDTO::getId) - .collect(Collectors.toList())) - .stream().map(l -> View.convert(l, QueueSessionSummaryDTO.class)) - .filter(qs -> qs.getSlot().today()) - .sorted(Comparator.comparing(qs -> qs.getSlot().getClosesAt())) - .collect(Collectors.toList())); - model.addAttribute("runningLabs", runningLabs); - } - - if (pd.getDefaultRole() == ADMIN || pd.getDefaultRole() == TEACHER) { - model.addAttribute("allEditions", eCache.get( - Objects.requireNonNullElse( - eApi.getAllEditionsActiveDuringPeriod( - new Period().start(LocalDateTime.now()) - .end(LocalDateTime.now().plusYears(1))) - .map(EditionSummaryDTO::getId) - .collectList().block(), - List.of()))); - } - model.addAttribute("editions", editions); model.addAttribute("sharedEditions", sharedEditions); model.addAttribute("sharedLabs", sharedLabs); diff --git a/src/main/java/nl/tudelft/queue/controller/LabController.java b/src/main/java/nl/tudelft/queue/controller/LabController.java index a8dc526c63fb64c96f46fbea4df8132b99271f4b..083a3e3257c1bfc254b82ec8041b7064a1b08b43 100644 --- a/src/main/java/nl/tudelft/queue/controller/LabController.java +++ b/src/main/java/nl/tudelft/queue/controller/LabController.java @@ -528,7 +528,9 @@ public class LabController { ls.deleteSession(qSession); var session = sCache.getOrThrow(qSession.getSession()); - // TODO: Redirect to an edition collection specific page for shared labs + if (session.getEditionCollection() != null) { + return "redirect:/shared-edition/" + session.getEditionCollection().getId(); + } return "redirect:/edition/" + session.getEditions().get(0).getId() + "/labs"; } diff --git a/src/main/java/nl/tudelft/queue/controller/RequestController.java b/src/main/java/nl/tudelft/queue/controller/RequestController.java index 38e8139c8a436f192232db7189b145762b854f45..61489cb9530c2054563d63d8e65a9259355a29e1 100644 --- a/src/main/java/nl/tudelft/queue/controller/RequestController.java +++ b/src/main/java/nl/tudelft/queue/controller/RequestController.java @@ -46,6 +46,7 @@ import nl.tudelft.queue.repository.LabRequestRepository; import nl.tudelft.queue.service.*; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; @@ -144,10 +145,16 @@ public class RequestController { .map(qs -> (Lab) qs) .collect(Collectors.toList()); + List<LabRequest> filteredRequests = rs + .filterRequestsSharedEditionCheck(lrr.findAllByFilter(labs, filter, pageable).getContent(), + assistant); + + var requestsViews = rts.convertRequestsToView( + new PageImpl<>(filteredRequests, pageable, filteredRequests.size()), filteredRequests.size()); + model.addAttribute("page", "requests"); model.addAttribute("filter", filter); - model.addAttribute("requests", rts.convertRequestsToView( - lrr.findAllByFilter(labs, filter, pageable), lrr.countByFilter(labs, filter))); + model.addAttribute("requests", requestsViews); model.addAttribute("requestCounts", rts.labRequestCounts( labs, assistant, filter)); diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java index 2ac52e1a5efd9ef78d0c80b4ddbeaaa5af6beaef..480c00ca34b92c714e550127577d66deb9bad5b2 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java @@ -44,7 +44,7 @@ public abstract class QueueSessionCreateDTO<D extends QueueSession<?>> extends C private Set<Long> modules = new HashSet<>(); @Builder.Default - private Set<Long> rooms = new HashSet<>(); + protected Set<Long> rooms = new HashSet<>(); @Builder.Default private LabRequestConstraintsCreateDTO constraints = new LabRequestConstraintsCreateDTO(); @@ -74,7 +74,6 @@ public abstract class QueueSessionCreateDTO<D extends QueueSession<?>> extends C nonEmpty("modules", modules); - nonEmpty("rooms", rooms); } @Override diff --git a/src/main/java/nl/tudelft/queue/dto/create/labs/CapacitySessionCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/labs/CapacitySessionCreateDTO.java index 87d9c921e7785034fa9f3bc2c454f194501eed9e..62e7df05b95b0224cab86147a31e98bf2997ec89 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/labs/CapacitySessionCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/labs/CapacitySessionCreateDTO.java @@ -55,6 +55,8 @@ public class CapacitySessionCreateDTO extends QueueSessionCreateDTO<CapacitySess public void validate() { super.validate(); + nonEmpty("rooms", rooms); + if (capacitySessionConfig.getEnrolmentClosesAt() .isBefore(capacitySessionConfig.getEnrolmentOpensAt())) { errors.rejectValue("capacitySessionConfig.enrolmentClosesAt", diff --git a/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java index 1845586e73a64f611464be03d3439474d0cecb26..8ddd9ea4af5bea4e65a2d09390ca3d84bac73ff4 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java @@ -18,6 +18,7 @@ package nl.tudelft.queue.dto.create.labs; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,9 +34,12 @@ import nl.tudelft.labracore.api.dto.SessionDetailsDTO; import nl.tudelft.queue.dto.create.QueueSessionCreateDTO; import nl.tudelft.queue.model.embeddables.AllowedRequest; import nl.tudelft.queue.model.enums.CommunicationMethod; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.labs.Lab; +import org.springframework.util.CollectionUtils; + @Data @SuperBuilder @NoArgsConstructor @@ -51,6 +55,9 @@ public abstract class LabCreateDTO<D extends Lab> extends QueueSessionCreateDTO< @Builder.Default private Map<Long, Set<RequestType>> requestTypes = new HashMap<>(); + @Builder.Default + private Set<OnlineMode> onlineModes = new HashSet<>(); + @Builder.Default private Boolean enableExperimental = false; @@ -65,6 +72,8 @@ public abstract class LabCreateDTO<D extends Lab> extends QueueSessionCreateDTO< .collect(Collectors.groupingBy(AllowedRequest::getAssignment, Collectors.mapping(AllowedRequest::getType, Collectors.toSet()))); + this.onlineModes = new HashSet<>(lab.getOnlineModes()); + this.enableExperimental = lab.getEnableExperimental(); } @@ -77,6 +86,12 @@ public abstract class LabCreateDTO<D extends Lab> extends QueueSessionCreateDTO< nonEmpty("requestTypes", requestTypes); nonNull("enableExperimental", enableExperimental); + + if (CollectionUtils.isEmpty(rooms) && CollectionUtils.isEmpty(onlineModes)) { + errors.rejectValue("rooms", "Select at least 1 room or online mode"); + errors.rejectValue("onlineModes", "Select at least 1 room or online mode"); + } + } @Override diff --git a/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java index f09d6ff1c1d6e92e09b66be4ded7cf3d84eafd16..8b535e49adc7ccb51a75fe5bbc6194b54752fb59 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java @@ -35,6 +35,7 @@ import nl.tudelft.queue.dto.create.RequestCreateDTO; import nl.tudelft.queue.dto.id.TimeSlotIdDTO; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.model.embeddables.AllowedRequest; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.labs.Lab; import nl.tudelft.queue.model.labs.SlottedLab; @@ -60,9 +61,10 @@ public class LabRequestCreateDTO extends RequestCreateDTO<LabRequest, Lab> { @NotNull private Long assignment; - @NotNull private Long room; + private OnlineMode onlineMode; + private transient Lab session; @Override @@ -73,8 +75,13 @@ public class LabRequestCreateDTO extends RequestCreateDTO<LabRequest, Lab> { nonEmpty("question", question); } + if ((room == null) == (onlineMode == null)) { + errors.rejectValue("room", "Room or online mode not selected"); + errors.rejectValue("onlineMode", "Room or online mode not selected"); + } + var session = getBean(SessionCacheManager.class).getOrThrow(this.getSession().getSession()); - if (session.getRooms().stream().noneMatch(r -> Objects.equals(r.getId(), room))) { + if (room != null && session.getRooms().stream().noneMatch(r -> Objects.equals(r.getId(), room))) { errors.rejectValue("room", "A room with id " + room + " is not available in the lab."); } diff --git a/src/main/java/nl/tudelft/queue/dto/patch/LabPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/LabPatchDTO.java index cf70084da0f345957cee494442ae386e700a635b..abcb220d9ea369ebd5dda648b7bc5ec759546db3 100644 --- a/src/main/java/nl/tudelft/queue/dto/patch/LabPatchDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/patch/LabPatchDTO.java @@ -17,9 +17,7 @@ */ package nl.tudelft.queue.dto.patch; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import javax.validation.constraints.Max; @@ -28,7 +26,9 @@ import javax.validation.constraints.Min; import lombok.*; import lombok.experimental.SuperBuilder; import nl.tudelft.labracore.api.dto.AssignmentIdDTO; +import nl.tudelft.labracore.api.dto.RoomIdDTO; import nl.tudelft.queue.model.enums.CommunicationMethod; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.labs.Lab; @@ -47,12 +47,19 @@ public abstract class LabPatchDTO<D extends Lab> extends QueueSessionPatchDTO<D> @Builder.Default private Map<Long, Set<RequestType>> requestTypes = null; + @Builder.Default + private Set<OnlineMode> onlineModes = null; + private Boolean enableExperimental; @Override protected void applyOneToOne() { updateNonNull(communicationMethod, data::setCommunicationMethod); updateNonNull(eolGracePeriod, data::setEolGracePeriod); + if (onlineModes == null && rooms != null) { + onlineModes = new HashSet<>(); + } + updateNonNull(onlineModes, data::setOnlineModes); data.setEnableExperimental(Boolean.TRUE.equals(enableExperimental)); // null is false as well } @@ -65,7 +72,18 @@ public abstract class LabPatchDTO<D extends Lab> extends QueueSessionPatchDTO<D> @Override protected void validate() { + super.validate(); + nonEmpty("requestTypes", requestTypes); + + // A null value does not update labracore or queue, therefore we set it to an empty list. + // No verification needed to check + if (rooms == null && onlineModes != null) { + rooms = new HashSet<>(); + } else if (onlineModes == null && rooms != null) { + onlineModes = new HashSet<>(); + } + } @Override @@ -80,6 +98,24 @@ public abstract class LabPatchDTO<D extends Lab> extends QueueSessionPatchDTO<D> .collect(Collectors.toList()); } + /** + * Overriden method, since if it returns null, it usually signifies "no change" in labracore. From now it + * will signify that this is a sesion with online modes only. + * + * @return A list of roomIdDTOs + */ + @Override + public List<RoomIdDTO> roomIdDTOs() { + if (rooms == null) { + if (onlineModes != null) { + return new ArrayList<>(); + } + return null; + } + + return rooms.stream().map(al -> new RoomIdDTO().id(al)).collect(Collectors.toList()); + } + private void nonEmpty(String field, Map<?, ?> set) { if (set != null && set.isEmpty()) { errors.rejectValue(field, "Field should not be empty"); diff --git a/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java index 631d5945cbec87bf6f7b240b8d7f895082dc5c39..72d862c244b4755ececd69e2a1ce9f85294c781c 100644 --- a/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java @@ -44,7 +44,7 @@ public abstract class QueueSessionPatchDTO<D extends QueueSession<?>> extends Pa private Set<Long> modules = null; @Builder.Default - private Set<Long> rooms = null; + protected Set<Long> rooms = null; @Builder.Default private LabRequestConstraintsPatchDTO constraints = new LabRequestConstraintsPatchDTO(); @@ -64,7 +64,6 @@ public abstract class QueueSessionPatchDTO<D extends QueueSession<?>> extends Pa @Override protected void validate() { nonEmpty("modules", modules); - nonEmpty("rooms", rooms); } /** @@ -83,7 +82,7 @@ public abstract class QueueSessionPatchDTO<D extends QueueSession<?>> extends Pa return rooms.stream().map(al -> new RoomIdDTO().id(al)).collect(Collectors.toList()); } - private void nonEmpty(String field, Collection<?> set) { + protected void nonEmpty(String field, Collection<?> set) { if (set != null && set.isEmpty()) { errors.rejectValue(field, "Field should not be empty"); } diff --git a/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java index 25294984fcfc48b73e22c4faf92e534074aad135..c6f18b3bf902a343f78c1dbfc1c4c47534cc2d54 100644 --- a/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java @@ -48,7 +48,7 @@ public class RequestPatchDTO extends Patch<LabRequest> { protected void validate() { var session = SpringContext.getBean(SessionCacheManager.class) .getOrThrow(data.getSession().getSession()); - if (session.getRooms().stream().noneMatch(r -> Objects.equals(r.getId(), room))) { + if (room != null && session.getRooms().stream().noneMatch(r -> Objects.equals(r.getId(), room))) { errors.rejectValue("room", "Room is not found in the surrounding lab"); } diff --git a/src/main/java/nl/tudelft/queue/dto/patch/labs/CapacitySessionPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/labs/CapacitySessionPatchDTO.java index cae2edb250867e4c5fcedd103abc74fa348f2776..4d8b8ec28c5ed51d6b5d2a5a6a6e097120c54245 100644 --- a/src/main/java/nl/tudelft/queue/dto/patch/labs/CapacitySessionPatchDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/patch/labs/CapacitySessionPatchDTO.java @@ -40,6 +40,12 @@ public class CapacitySessionPatchDTO extends QueueSessionPatchDTO<CapacitySessio @Builder.Default private CapacitySessionConfigPatchDTO capacitySessionConfig = new CapacitySessionConfigPatchDTO(); + @Override + public void validate() { + super.validate(); + nonEmpty("rooms", rooms); + } + @Override protected void postApply() { super.postApply(); diff --git a/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java index 86ef532e4ccc81cfb7ba17eec0c75d60af72fb23..77fc3722a3ed8be2424757f9d6da5f46c76d6847 100644 --- a/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java @@ -29,6 +29,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import nl.tudelft.librador.dto.Validated; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.enums.RequestType; @@ -43,6 +44,9 @@ public class RequestTableFilterDTO extends Validated implements Serializable { private Set<Long> assignments = new HashSet<>(); @NotNull private Set<Long> rooms = new HashSet<>(); + + @NotNull + private Set<OnlineMode> onlineModes = new HashSet<>(); @NotNull private Set<Long> assigned = new HashSet<>(); @NotNull @@ -60,7 +64,7 @@ public class RequestTableFilterDTO extends Validated implements Serializable { * @return The number of lists which contain at least 1 element to filter on. */ public long countActiveFilters() { - return Stream.of(labs, assigned, rooms, assigned, assignments, requestStatuses, + return Stream.of(labs, assigned, rooms, onlineModes, assigned, assignments, requestStatuses, requestTypes).filter(s -> !s.isEmpty()).count(); } @@ -70,7 +74,7 @@ public class RequestTableFilterDTO extends Validated implements Serializable { * @return A stream of Sets. */ private Stream<Set<?>> getAllFiltersAsStream() { - return Stream.of(labs, assigned, rooms, assigned, assignments, requestStatuses, + return Stream.of(labs, assigned, rooms, onlineModes, assigned, assignments, requestStatuses, requestTypes); } diff --git a/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java index e7799ea679224a2de128a21f86d69bf3e439156a..60ea91065092e043bf36c000ae2dcadf5af01d9d 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java @@ -22,6 +22,7 @@ import java.util.Set; import lombok.*; import nl.tudelft.queue.model.embeddables.AllowedRequest; import nl.tudelft.queue.model.enums.CommunicationMethod; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.labs.Lab; @Data @@ -34,4 +35,7 @@ public abstract class LabViewDTO<D extends Lab> extends QueueSessionViewDTO<D> { private Set<AllowedRequest> allowedRequests; private Boolean enableExperimental; + + private Set<OnlineMode> onlineModes; + } diff --git a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java index 16cad7b0f5aab595ee730635a90cb6af3fd6d672..fa7fa48c3cf78d5cec835f680a4f55aa1602a19b 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java @@ -21,12 +21,16 @@ import static java.time.LocalDateTime.now; import static nl.tudelft.librador.SpringContext.getBean; import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import nl.tudelft.labracore.api.dto.EditionSummaryDTO; import nl.tudelft.librador.dto.view.View; +import nl.tudelft.queue.cache.EditionCollectionCacheManager; import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.misc.QueueSessionStatus; import nl.tudelft.queue.model.QueueSession; @@ -46,6 +50,8 @@ public class QueueSessionSummaryDTO extends View<QueueSession<?>> { private QueueSessionType type; + private List<EditionSummaryDTO> associatedEditions; + private String name; private String readableName; private Slot slot; @@ -84,6 +90,14 @@ public class QueueSessionSummaryDTO extends View<QueueSession<?>> { .isBefore(now()); } isShared = session.getEditionCollection() != null; + if (isShared) { + var editionCollection = getBean(EditionCollectionCacheManager.class) + .getOrThrow(session.getEditionCollection().getId()); + associatedEditions = editionCollection.getEditions(); + } else { + associatedEditions = List.of(Objects.requireNonNull(session.getEdition())); + } + } /** diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java index cae29aee2f293b9fa4f7164c5c8f3fa44a6123bc..935d4197bf49a005c6ea4b98cb4255be510e046a 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java @@ -39,11 +39,12 @@ public class StudentNotFoundEventViewDTO extends RequestEventViewDTO<StudentNotF */ @Override public String getDescription() { + if (request.getOnlineMode() != null) { + return "Student did not connect on time"; + } switch (getRequest().getSession().getCommunicationMethod()) { - case JITSI_MEET: - return "Student did not connect on time"; case STUDENT_VISIT_TA: - return "Student did not show"; + return "Student did not show up"; case TA_VISIT_STUDENT: return "Could not find student to handle the request"; default: diff --git a/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java index 67072fb3a5e71543b59b758ed1dbdc8f73aaab2b..a861a969294985f9d9277ad73eba64ab17916740 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java @@ -31,9 +31,11 @@ import nl.tudelft.queue.cache.AssignmentCacheManager; import nl.tudelft.queue.cache.EditionCacheManager; import nl.tudelft.queue.cache.ModuleCacheManager; import nl.tudelft.queue.dto.view.RequestViewDTO; +import nl.tudelft.queue.dto.view.events.RequestHandledEventViewDTO; import nl.tudelft.queue.model.Feedback; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.model.TimeSlot; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestType; import org.apache.commons.lang3.ArrayUtils; @@ -50,6 +52,7 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> { private String jitsiRoom; private TimeSlot timeSlot; + private OnlineMode onlineMode; private RequestType requestType; @@ -57,10 +60,18 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> { private List<Feedback> feedbacks; + private String commentForStudent; + + private String commentForAssistant; + @Override public void postApply() { super.postApply(); + if (onlineMode != null) { + setRoom(null); + } + assignment = getBean(AssignmentCacheManager.class).getOrThrow(data.getAssignment()); timeSlot = data.getTimeSlot(); @@ -69,6 +80,17 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> { .getEdition().getId(); super.setEdition(getBean(EditionCacheManager.class).getOrThrow(editionId)); } + + var handledEvent = getEvents().stream() + .filter(e -> e instanceof RequestHandledEventViewDTO<?>) + .findFirst() + .map(e -> (RequestHandledEventViewDTO<?>) e); + if (handledEvent.isPresent()) { + commentForStudent = handledEvent.get().getReasonForStudent(); + commentForAssistant = handledEvent.get().getReasonForAssistant(); + } else { + commentForStudent = commentForAssistant = ""; + } } @Override @@ -93,8 +115,11 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> { "Comment", "Question", "Time Slot", + "Online Mode", "Request Type", - "Assignment"); + "Assignment", + "Reason for Student", + "Reason for Assistant"); } @Override @@ -103,7 +128,10 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> { (comment != null) ? comment : "", (question != null) ? question : "", (timeSlot != null) ? timeSlot.toString() : "", + (onlineMode != null) ? onlineMode.getDisplayName() : "", requestType.displayName(), - assignment.getName()); + assignment.getName(), + commentForStudent, + commentForAssistant); } } diff --git a/src/main/java/nl/tudelft/queue/model/Feedback.java b/src/main/java/nl/tudelft/queue/model/Feedback.java index bce2fc2516520e68d2a94b22bb938bf669e90109..670767ed565f6d0218da0dadeabaed3a9d90f27f 100644 --- a/src/main/java/nl/tudelft/queue/model/Feedback.java +++ b/src/main/java/nl/tudelft/queue/model/Feedback.java @@ -32,6 +32,9 @@ import lombok.Data; import lombok.NoArgsConstructor; import nl.tudelft.queue.model.labs.Lab; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + /** * Entity class representing feedback given to a TA about one request they contributed to handling. Feedback * may be given multiple times per request, but can only given once per TA per request. In other words, one @@ -42,6 +45,8 @@ import nl.tudelft.queue.model.labs.Lab; @Builder @NoArgsConstructor @AllArgsConstructor +@Where(clause = "deleted_at IS NULL") +@SQLDelete(sql = "UPDATE feedback SET deleted_at = NOW() WHERE request_id = ? AND assistant_id = ?") public class Feedback { /** * The embeddable id of a Feedback object. @@ -108,4 +113,7 @@ public class Feedback { @NotNull @Builder.Default private LocalDateTime lastUpdatedAt = LocalDateTime.now(); + + @Builder.Default + private LocalDateTime deletedAt = null; } diff --git a/src/main/java/nl/tudelft/queue/model/LabRequest.java b/src/main/java/nl/tudelft/queue/model/LabRequest.java index d520298e9d53e5cbca493c465305c34306dfea9f..16631a57c4818899992fd58fcb63772d38a67a96 100644 --- a/src/main/java/nl/tudelft/queue/model/LabRequest.java +++ b/src/main/java/nl/tudelft/queue/model/LabRequest.java @@ -33,10 +33,13 @@ import lombok.*; import lombok.experimental.SuperBuilder; import nl.tudelft.librador.dto.view.View; import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.labs.ExamLab; import nl.tudelft.queue.model.labs.Lab; +import org.hibernate.annotations.Cascade; + /** * A request that has been made by a StudentGroup, that the database representation of someone asking for * help. Part of this class stores it's data directly in the Request table. The lifecycle of a Request, that @@ -72,6 +75,11 @@ public class LabRequest extends Request<Lab> { */ private String jitsiRoom; + /** + * The OnlineMode this request will take place in (Jitsi, MS teams, etc) + */ + private OnlineMode onlineMode; + /** * The timeslot this request occupies. Timeslots can be used as opposed to direct requests to allow for * reserving a TA for a specific time ahead of the lab starting. @@ -94,6 +102,7 @@ public class LabRequest extends Request<Lab> { @ToString.Exclude @EqualsAndHashCode.Exclude @OneToMany(mappedBy = "request") + @Cascade(org.hibernate.annotations.CascadeType.DELETE) private List<Feedback> feedbacks = new ArrayList<>(); /** diff --git a/src/main/java/nl/tudelft/queue/model/LabRequestConstraint.java b/src/main/java/nl/tudelft/queue/model/LabRequestConstraint.java index 5754b107f1b36c4c5f61532fd499068842d4cd6c..03b7dbafc7389c9b9118f40a2c7d45d4b4a03c74 100644 --- a/src/main/java/nl/tudelft/queue/model/LabRequestConstraint.java +++ b/src/main/java/nl/tudelft/queue/model/LabRequestConstraint.java @@ -18,6 +18,7 @@ package nl.tudelft.queue.model; import javax.persistence.*; +import javax.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Data; @@ -94,5 +95,5 @@ public abstract class LabRequestConstraint { * * @param session The lab to set. */ - public abstract void setSession(QueueSession<?> session); + public abstract void setSession(@NotNull QueueSession<?> session); } diff --git a/src/main/java/nl/tudelft/queue/model/enums/CommunicationMethod.java b/src/main/java/nl/tudelft/queue/model/enums/CommunicationMethod.java index a96e9575c76533d491eae97ad1d71fff16eab5b7..5a808afa535004eb91e37d8e06dc066be7c9af45 100644 --- a/src/main/java/nl/tudelft/queue/model/enums/CommunicationMethod.java +++ b/src/main/java/nl/tudelft/queue/model/enums/CommunicationMethod.java @@ -31,17 +31,8 @@ public enum CommunicationMethod { ), STUDENT_VISIT_TA( "Student visits TA" - ), - JITSI_MEET( - "Jitsi Meeting" ); private final String displayName; - /** - * @return Whether the communication method is an online method. - */ - public boolean isOnline() { - return JITSI_MEET == this; - } } diff --git a/src/main/java/nl/tudelft/queue/model/enums/OnlineMode.java b/src/main/java/nl/tudelft/queue/model/enums/OnlineMode.java new file mode 100644 index 0000000000000000000000000000000000000000..318871b2e271d7383d1415170c21267d8ec32595 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/model/enums/OnlineMode.java @@ -0,0 +1,48 @@ +/* + * Queue - A Queueing system that can be used to handle labs in higher education + * Copyright (C) 2016-2021 Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.queue.model.enums; + +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum OnlineMode { + + JITSI( + "Jitsi" + ); + + private final String displayName; + + /** + * Maps the online mode(s) to the display name of the respective mode(s) and concatenates them. Used + * mainly for displaying the available online modes to end users. + * + * @param onlineModes A set of online modes + * @return A concatenated string with all the display names + */ + public static String displayNames(Set<OnlineMode> onlineModes) { + return onlineModes.stream().map(OnlineMode::getDisplayName) + .sorted(String::compareToIgnoreCase) + .collect(Collectors.joining(", ")); + } +} diff --git a/src/main/java/nl/tudelft/queue/model/labs/Lab.java b/src/main/java/nl/tudelft/queue/model/labs/Lab.java index 5d88cf2213aa1e7ad40bef1dc2c8ecdbe372c9bf..0ced510627ef11c7247a541816426bf0c1df6f48 100644 --- a/src/main/java/nl/tudelft/queue/model/labs/Lab.java +++ b/src/main/java/nl/tudelft/queue/model/labs/Lab.java @@ -41,6 +41,7 @@ import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.Request; import nl.tudelft.queue.model.embeddables.AllowedRequest; import nl.tudelft.queue.model.enums.CommunicationMethod; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestType; import org.hibernate.validator.constraints.UniqueElements; @@ -54,8 +55,7 @@ import org.hibernate.validator.constraints.UniqueElements; public abstract class Lab extends QueueSession<LabRequest> { /** - * The communication method that will be used for the lab. This could range from physical contact to Jitsi - * meetings. + * The communication method that will be used for the lab. Online meetings disregard this. */ @NotNull @Enumerated(EnumType.STRING) @@ -80,6 +80,15 @@ public abstract class Lab extends QueueSession<LabRequest> { @ElementCollection private Set<AllowedRequest> allowedRequests = new HashSet<>(); + /** + * The online options available for the lab. Currently only Jitsi + */ + @NotNull + @UniqueElements + @Builder.Default + @ElementCollection + private Set<OnlineMode> onlineModes = new HashSet<>(); + @NotNull @Builder.Default private Boolean enableExperimental = false; @@ -129,8 +138,9 @@ public abstract class Lab extends QueueSession<LabRequest> { return request instanceof LabRequest && super.allowsRequest(request) && allowsRequest((LabRequest) request) - && request.getRoom() != null && session.getRooms().stream() - .anyMatch(room -> Objects.equals(room.getId(), request.getRoom())); + && (request.getRoom() == null) ? ((LabRequest) request).getOnlineMode() != null + : session.getRooms().stream() + .anyMatch(room -> Objects.equals(room.getId(), request.getRoom())); } /** diff --git a/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java b/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java index c0736c9bf9881553c33a3d161a0725368b889f4b..3ef20d8449cb8ccddd0fe2b5fa8bb20b3e829d8e 100644 --- a/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java +++ b/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java @@ -24,6 +24,7 @@ import nl.tudelft.librador.dto.view.View; import nl.tudelft.queue.cache.ModuleCacheManager; import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO; import nl.tudelft.queue.model.LabRequest; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.enums.RequestType; @@ -50,6 +51,10 @@ public class RequestCreatedMessage extends View<LabRequest> implements Message { private Long buildingId; private String buildingName; + private OnlineMode onlineMode; + + private String onlineModeDisplayName; + private Long assignmentId; private String assignmentName; private Long moduleId; @@ -84,10 +89,15 @@ public class RequestCreatedMessage extends View<LabRequest> implements Message { requestedBy = view.requesterEntityName(); - roomId = view.getRoom().getId(); - roomName = view.getRoom().getName(); - buildingId = view.getRoom().getBuilding().getId(); - buildingName = view.getRoom().getBuilding().getName(); + if (view.getRoom() != null) { + roomId = view.getRoom().getId(); + roomName = view.getRoom().getName(); + buildingId = view.getRoom().getBuilding().getId(); + buildingName = view.getRoom().getBuilding().getName(); + } else if (view.getOnlineMode() != null) { + onlineMode = view.getOnlineMode(); + onlineModeDisplayName = view.getOnlineMode().getDisplayName(); + } assignmentId = view.getAssignment().getId(); assignmentName = view.getAssignment().getName(); diff --git a/src/main/java/nl/tudelft/queue/repository/FeedbackRepository.java b/src/main/java/nl/tudelft/queue/repository/FeedbackRepository.java index dc91e820bb5f00804a18f34a810c8cc4cbb5bab0..9fb6e629554baa23120710c0a791c163f38677ed 100644 --- a/src/main/java/nl/tudelft/queue/repository/FeedbackRepository.java +++ b/src/main/java/nl/tudelft/queue/repository/FeedbackRepository.java @@ -88,11 +88,12 @@ public interface FeedbackRepository */ default Page<Feedback> findByAssistantAnonymised(Long assistantId, Pageable pageable) { var feedback = StreamSupport.stream(findAll(qf.id.assistantId.eq(assistantId) - .and(qf.feedback.isNotNull()).and(qf.feedback.isNotEmpty()) - .and(qf.createdAt.before(LocalDateTime.now().minusMonths(1))), qf.createdAt.desc()) + .and(qf.createdAt.before(LocalDateTime.now().minusWeeks(2))), qf.createdAt.desc()) .spliterator(), false) - .skip(10).collect(Collectors.toList()); + .skip(5) + .filter(f -> f.getFeedback() != null && !f.getFeedback().isEmpty()) + .collect(Collectors.toList()); var page = feedback.subList(pageable.getPageNumber() * pageable.getPageSize(), Math.min(feedback.size(), (pageable.getPageNumber() + 1) * pageable.getPageSize())); return new PageImpl<>(page, pageable, feedback.size()); diff --git a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java index 658c90981b4ea0f34eae60521f6ce2acf0853746..35b9e3943acdfd3f90cad1e1d8d482c2fa38ea52 100644 --- a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java +++ b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java @@ -35,10 +35,7 @@ import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.labs.AbstractSlottedLab; import nl.tudelft.queue.model.labs.Lab; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; +import org.springframework.data.domain.*; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.lang.NonNull; @@ -364,6 +361,7 @@ public interface LabRequestRepository e = and(e, filter.getAssigned(), qlr.eventInfo.assignedTo::in); e = and(e, filter.getLabs(), qlr.session.id::in); e = and(e, filter.getRooms(), qlr.room::in); + e = and(e, filter.getOnlineModes(), qlr.onlineMode::in); e = and(e, filter.getAssignments(), qlr.assignment::in); e = and(e, filter.getRequestStatuses(), qlr.eventInfo.status::in); e = and(e, filter.getRequestTypes(), qlr.requestType::in); diff --git a/src/main/java/nl/tudelft/queue/service/EditionService.java b/src/main/java/nl/tudelft/queue/service/EditionService.java index 3e00f24752d6179158fd4e53116e2a660e85a6f9..0fe80542f26985df1d7315e11d052588b54acbc9 100644 --- a/src/main/java/nl/tudelft/queue/service/EditionService.java +++ b/src/main/java/nl/tudelft/queue/service/EditionService.java @@ -23,6 +23,7 @@ import static java.util.stream.Collectors.groupingBy; import java.io.IOException; import java.util.*; +import java.util.List; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; diff --git a/src/main/java/nl/tudelft/queue/service/LabService.java b/src/main/java/nl/tudelft/queue/service/LabService.java index a9d5d601d2906c5a4e5157650f353a5c8170b613..efae7c7c1481bba5508bea30e44919361186524a 100644 --- a/src/main/java/nl/tudelft/queue/service/LabService.java +++ b/src/main/java/nl/tudelft/queue/service/LabService.java @@ -22,10 +22,7 @@ import static nl.tudelft.queue.misc.QueueSessionStatus.*; import java.io.IOException; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; import javax.transaction.Transactional; @@ -50,6 +47,7 @@ import nl.tudelft.queue.misc.QueueSessionStatus; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.TimeSlot; import nl.tudelft.queue.model.embeddables.AllowedRequest; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.enums.SelectionProcedure; import nl.tudelft.queue.model.labs.AbstractSlottedLab; @@ -414,7 +412,7 @@ public class LabService { * Get all the labs which are taking place within a certain period. * * @param period The period for which to retrieve all the labs. - * @return A list of labs which are all occuring within the specified period. + * @return A list of labs which are all occurring within the specified period. */ public List<Lab> getAllLabsWithinPeriod(Period period) { var editions = eCache.get(Objects.requireNonNull(eApi.getAllEditionsActiveDuringPeriod(period) @@ -434,11 +432,9 @@ public class LabService { */ public long countOngoingLabs(List<Lab> labs) { LocalDateTime now = LocalDateTime.now(); - return labs.stream().filter(l -> { - SessionDetailsDTO session = sCache.getOrThrow(l.getSession()); - return session.getStart().isBefore(now) && - session.getEnd().isAfter(now); - }).count(); + List<SessionDetailsDTO> sessions = sCache.get(labs.stream().map(QueueSession::getSession)); + return sessions.stream().filter(s -> s.getStart().isBefore(now) && + s.getEnd().isAfter(now)).count(); } /** @@ -455,7 +451,7 @@ public class LabService { case EXAM: AbstractSlottedLab<TimeSlot> lab = (AbstractSlottedLab<TimeSlot>) l; return lab.getSlottedLabConfig().getSelectionOpensAt().isBefore(now) - || sCache.getOrThrow(lab.getSession()).getStart().isAfter(now); + && sCache.getOrThrow(lab.getSession()).getEnd().isAfter(now); default: return false; } @@ -486,4 +482,15 @@ public class LabService { return allowedAssignments; } + /** + * Gets the online modes in a lab session. + * + * @param labSessionId The ID of the lab session + * @return The online modes associated to the lab. + */ + public Set<OnlineMode> getOnlineModesInLabSession(long labSessionId) { + var labs = lr.findAllBySessions(List.of(labSessionId)); + return labs.stream().map(Lab::getOnlineModes).flatMap(Set::stream).collect(Collectors.toSet()); + } + } diff --git a/src/main/java/nl/tudelft/queue/service/PermissionService.java b/src/main/java/nl/tudelft/queue/service/PermissionService.java index 6067bd32d6d119d9442d5b82e14ed751284061b0..85c2a68a6f1b6537c165a7b959a1481814a0c32e 100644 --- a/src/main/java/nl/tudelft/queue/service/PermissionService.java +++ b/src/main/java/nl/tudelft/queue/service/PermissionService.java @@ -330,7 +330,7 @@ public class PermissionService { * @return Whether the currently authenticated person has a staff role in any of the given * course editions. */ - private boolean hasStaffRole(List<Long> editionIds) { + public boolean hasStaffRole(List<Long> editionIds) { return withAnyRole(editionIds, (person, role) -> STAFF_ROLES.contains(role)); } @@ -400,6 +400,16 @@ public class PermissionService { return isAdmin() || withRole(editionId, (person, role) -> role != BLOCKED); } + /** + * Determine if a user has manage perms for a edition/shared edition. + * + * @param editionList The editions to check for + * @return true iff the person is allowed to modify said editions, false otherwise + */ + public boolean canManageInAnyEdition(List<EditionSummaryDTO> editionList) { + return editionList.stream().anyMatch(edition -> canManageSessions(edition.getId())); + } + /** * @param edition The edition the authenticated user wants to see. * @return Whether the authenticated user can see the edition. diff --git a/src/main/java/nl/tudelft/queue/service/RequestService.java b/src/main/java/nl/tudelft/queue/service/RequestService.java index f73c23c6f6dffad50ec882b6bfb59c41d185720e..b71df2242b7c2952ac932b879d248ebb38efa65f 100644 --- a/src/main/java/nl/tudelft/queue/service/RequestService.java +++ b/src/main/java/nl/tudelft/queue/service/RequestService.java @@ -24,6 +24,7 @@ import java.util.stream.Collectors; import javax.transaction.Transactional; +import nl.tudelft.labracore.api.AssignmentControllerApi; import nl.tudelft.labracore.api.ModuleControllerApi; import nl.tudelft.labracore.api.QuestionControllerApi; import nl.tudelft.labracore.api.StudentGroupControllerApi; @@ -36,6 +37,7 @@ import nl.tudelft.queue.model.ClosableTimeSlot; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.model.Request; import nl.tudelft.queue.model.SelectionRequest; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.SelectionProcedure; import nl.tudelft.queue.model.events.*; import nl.tudelft.queue.model.labs.AbstractSlottedLab; @@ -79,6 +81,12 @@ public class RequestService { @Autowired private QuestionControllerApi qApi; + @Autowired + private AssignmentControllerApi asApi; + + @Autowired + private RoleDTOService roleDTOService; + @Autowired private PersonCacheManager pCache; @@ -98,6 +106,9 @@ public class RequestService { @Lazy private LabService lService; + @Autowired + private PermissionService permissionService; + private static final ReentrantLock lock = new ReentrantLock(); /** @@ -123,7 +134,12 @@ public class RequestService { if (request instanceof LabRequest) { var labRequest = (LabRequest) request; - if (((LabRequest) request).getSession().getCommunicationMethod().isOnline()) { + /* + * TODO in the future, delegate to another service layer component, + * and let it generate a link for the respective online mode. + * This is for the far future, when more online modes need to be supported. + */ + if (((LabRequest) request).getOnlineMode() == OnlineMode.JITSI) { labRequest.setJitsiRoom(js.createJitsiRoomName(labRequest)); } if (((LabRequest) request).getSession().getEnableExperimental()) { @@ -395,7 +411,6 @@ public class RequestService { Optional<LabRequest> oldRequests = lrr.findCurrentlyProcessingRequest(assistant, request.getSession()); if (oldRequests.isPresent()) { - lock.unlock(); return oldRequests.get(); } rer.applyAndSave(new RequestPickedEvent(request, assistant.getId())); @@ -500,6 +515,34 @@ public class RequestService { }); } + /** + * This is meant to act as a filter for a list of lab requests that originates from a shared session. This + * is so that assistants from other editions don't see requests from other editions they are not an + * assistant of. + * + * @param requests A list of requests to filter + * @param person The assistant that will see the lab requests + * @return A new list of lab requests that the assistant will see only if they are an assistant + * of the edition the request belongs. + */ + public List<LabRequest> filterRequestsSharedEditionCheck(List<LabRequest> requests, Person person) { + // Check to make sure requests are not empty, otherwise API call fails. + if (requests.isEmpty()) { + return requests; + } + + var assignmentIds = requests.stream().map(LabRequest::getAssignment).distinct().toList(); + + var allowedAssignments = Objects + .requireNonNull(asApi.getAssignmentsWithModules(assignmentIds).collectList().block()).stream() + .filter(assignment -> permissionService + .hasStaffRole(List.of(assignment.getModule().getEdition().getId()))) + .map(AssignmentModuleDetailsDTO::getId) + .collect(Collectors.toSet()); + + return requests.stream().filter(r -> allowedAssignments.contains(r.getAssignment())).toList(); + } + /** * Creates an individual student group for one student without a current group so that they may place a * request with that student group. diff --git a/src/main/java/nl/tudelft/queue/service/WebSocketService.java b/src/main/java/nl/tudelft/queue/service/WebSocketService.java index 65684a1a89176b3f5c2412b5b58cf56b4282fb3c..7d6f840f821de638736dac99d9d0fc33f81f58ee 100644 --- a/src/main/java/nl/tudelft/queue/service/WebSocketService.java +++ b/src/main/java/nl/tudelft/queue/service/WebSocketService.java @@ -23,10 +23,12 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; +import nl.tudelft.labracore.api.AssignmentControllerApi; import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.SessionControllerApi; import nl.tudelft.labracore.api.StudentGroupControllerApi; @@ -44,6 +46,8 @@ import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + @Service public class WebSocketService { @@ -53,6 +57,9 @@ public class WebSocketService { @Autowired private SessionControllerApi sApi; + @Autowired + private AssignmentControllerApi aApi; + @Autowired private StudentGroupControllerApi sgApi; @@ -92,18 +99,26 @@ public class WebSocketService { * @param message The message containing information on the event that occurred for the request. */ @Async - public void sendRequestTableMessage(Request<?> request, Message message) { - var sMono = sApi.getSessionsById(List.of(request.getSession().getSession())).collectList(); - var rMono = sMono - .flatMapIterable(ss -> ss.stream() - .flatMap(s -> s.getEditions().stream().map(EditionSummaryDTO::getId)) - .collect(Collectors.toSet())) - .flatMap(id -> eApi.getEditionParticipants(id)); + public CompletableFuture<Void> sendRequestTableMessage(Request<?> request, Message message) { + Flux<RolePersonDetailsDTO> rMono; + if (request instanceof LabRequest labRequest) { + rMono = aApi.getAssignmentById(labRequest.getAssignment()) + .flatMapMany(dto -> eApi.getEditionParticipants(dto.getModule().getEdition().getId())); + } else { + var sMono = sApi.getSessionsById(List.of(request.getSession().getSession())).collectList(); + rMono = sMono + .flatMapIterable(ss -> ss.stream() + .flatMap(s -> s.getEditions().stream().map(EditionSummaryDTO::getId)) + .collect(Collectors.toSet())) + .flatMap(id -> eApi.getEditionParticipants(id)); + } sendMessage(Objects.requireNonNull(rMono .filter(r -> r != null && Set.of(TEACHER, TEACHER_RO, HEAD_TA, TA).contains(r.getType())) .map(RolePersonDetailsDTO::getPerson) .distinct().collectList().block()), "/topic/request-table", message); + + return CompletableFuture.completedFuture(null); } /** diff --git a/src/main/resources/migrations.yml b/src/main/resources/migrations.yml index b229e6f9f62cf2879a04c90d2422a08543c62eb3..79450ef225ed16ff8510a6db110f178761cc4839 100644 --- a/src/main/resources/migrations.yml +++ b/src/main/resources/migrations.yml @@ -795,3 +795,111 @@ databaseChangeLog: name: deleted_at type: TIMESTAMP tableName: request + # Hybrid lab schema changes + - changeSet: + id: 1665319404891-1 + author: hpage (generated) + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + name: lab_id + type: BIGINT + - column: + name: online_modes + type: INT + tableName: lab_online_modes + - changeSet: + id: 1665319404891-2 + author: hpage (generated) + changes: + - addColumn: + columns: + - column: + name: online_mode + type: INTEGER + tableName: lab_request + - changeSet: + id: 1665319404891-3 + author: hpage (generated) + changes: + - createIndex: + columns: + - column: + name: lab_id + indexName: FK5rmsu88a1ow9m60756pckpo4_INDEX_8 + tableName: lab_online_modes + - changeSet: + id: 1665319404891-4 + author: hpage (generated) + changes: + - addForeignKeyConstraint: + baseColumnNames: lab_id + baseTableName: lab_online_modes + constraintName: FK5rmsu88a1ow9m60756pckpo4 + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: lab + validate: true + - changeSet: + id: update-jitsi-direction-modes + author: Ruben Backx + changes: + - sql: + comment: Set all existing online labs to JITSI online mode + sql: insert into lab_online_modes (lab_id, online_modes) select id, 0 from lab where communication_method = 'JITSI_MEET'; + - changeSet: + id: update-jitsi-direction-request-modes-1 + author: Ruben Backx + changes: + - sql: + comment: (MariaDB) Set all existing online requests to JITSI online mode + dbms: mariadb + sql: update lab_request as lr inner join lab as l inner join request as r on r.session_id = l.id and r.id = lr.id set lr.online_mode = 0 where l.communication_method = 'JITSI_MEET'; + - changeSet: + id: update-jitsi-direction-request-modes-2 + author: Ruben Backx + changes: + - sql: + comment: (PGSQL) Set all existing online requests to JITSI online mode + dbms: postgresql + sql: update lab_request as lr set online_mode = 0 from lab as l, request as r where r.session_id = l.id and r.id = lr.id and l.communication_method = 'JITSI_MEET'; + - changeSet: + id: 1665319404891-M1 + author: Henry Page + changes: + - sql: + comment: Change communication method Jitsi to TA Visit Student + sql: update lab set communication_method = 'TA_VISIT_STUDENT' WHERE communication_method = 'JITSI_MEET'; + # Soft delete feedback and set deletion time + - changeSet: + id: 1668721308765-1 + author: henry (generated) + changes: + - addColumn: + columns: + - column: + name: deleted_at + type: TIMESTAMP + tableName: feedback + - changeSet: + id: 1668721308765-M1 + author: Henry Page + changes: + - sql: + comment: (MariaDB) Update feedbacks that have deleted requests to have same deletion time + dbms: mariadb + sql: UPDATE feedback AS f INNER JOIN request AS r ON r.id=f.request_id SET f.deleted_at = r.deleted_at WHERE r.deleted_at IS NOT NULL; + - changeSet: + id: 1668721308765-M2 + author: Henry Page + changes: + - sql: + comment: (PGSQL) Update feedbacks that have deleted requests to have same deletion time + dbms: postgresql + sql: UPDATE feedback AS f SET deleted_at = r.deleted_at FROM request AS r WHERE f.request_id = r.id AND r.deleted_at IS NOT NULL; diff --git a/src/main/resources/static/js/request_table.js b/src/main/resources/static/js/request_table.js index df87bf80b77b1b01a714f1cdecdd6aefb14f4a5a..74ff96666accf63edbf90ae253c8e35bb39509cd 100644 --- a/src/main/resources/static/js/request_table.js +++ b/src/main/resources/static/js/request_table.js @@ -41,6 +41,7 @@ const inFilter = (() => { let selectedLabs; let selectedAssignments; let selectedRooms; + let selectedOnlineModes; let selectedStatuses; let selectedTypes; @@ -49,17 +50,22 @@ const inFilter = (() => { selectedLabs = findSelected($("#lab-select")); selectedAssignments = findSelected($("#assignment-select")); selectedRooms = findSelected($("#room-select")); + selectedOnlineModes = findSelected($("#onlineMode-select")); selectedStatuses = findSelected($("#status-select")); selectedTypes = findSelected($("#request-type-select")); }); // Return a function checking whether an incoming request passes the filters return event => { + const locationConstraintMet = + (event["roomId"] && !event["onlineMode"]) ? selectedRooms.includes(event["roomId"].toString()) : + (!event["roomId"] && event["onlineMode"]) ? selectedOnlineModes.includes(event["onlineMode"]) : false; + return selectedLabs.includes(event["labId"].toString()) && selectedAssignments.includes(event["assignmentId"].toString()) && - selectedRooms.includes(event["roomId"].toString()) && selectedStatuses.includes(event["status"]) && - selectedTypes.includes(event["requestType"]); + selectedTypes.includes(event["requestType"]) && + locationConstraintMet; } }).apply() diff --git a/src/main/resources/templates/admin/view.html b/src/main/resources/templates/admin/view.html index 869371b05ef44a58cadd70df49524a3d425607df..578b67c34aa5aa421f5e6e112a8a3c4a34ce3201 100644 --- a/src/main/resources/templates/admin/view.html +++ b/src/main/resources/templates/admin/view.html @@ -36,10 +36,16 @@ </section> <ul class="nav nav-tabs"> + <li class="nav-item"> + <a th:href="@{/admin/running}" class="nav-link" + th:classappend="${#request.requestURI.matches('.*/running.*') ? 'active' : ''}"> + <i class="fa fa-th-large" aria-hidden="true"></i>Running Labs + </a> + </li> <li class="nav-item"> <a th:href="@{/admin/courses}" class="nav-link" th:classappend="${#request.requestURI.matches('.*/course.*') ? 'active' : ''}"> - <i class="fa fa-th-large" aria-hidden="true"></i>Courses + <i class="fa fa-th-large" aria-hidden="true"></i>All courses </a> </li> <li class="nav-item"> @@ -51,13 +57,13 @@ <li class="nav-item"> <a th:href="@{/admin/rooms}" class="nav-link" th:classappend="${#request.requestURI.matches('.*/room.*') ? 'active' : ''}"> - <i class="fa fa-flask" aria-hidden="true"></i> Rooms + <i class="fa fa-flask" aria-hidden="true"></i>Rooms </a> </li> <li class="nav-item"> <a th:href="@{/admin/announcements}" class="nav-link" th:classappend="${#request.requestURI.matches('.*/announcement.*') ? 'active' : ''}"> - <i class="fa fa-bullhorn" aria-hidden="true"></i> Announcements + <i class="fa fa-bullhorn" aria-hidden="true"></i>Announcements </a> </li> </ul> diff --git a/src/main/resources/templates/admin/view/calendar.html b/src/main/resources/templates/admin/view/calendar.html index c88d11d5ab77f56bd3f4b1f501ae74d032d827af..3ef86ba86a3015b9d5871f796ff55355f642053b 100644 --- a/src/main/resources/templates/admin/view/calendar.html +++ b/src/main/resources/templates/admin/view/calendar.html @@ -12,20 +12,6 @@ <section layout:fragment="subcontent"> <div class="container"> - <div class="alert alert-warning mt-md-3" role="alert" - th:unless="${#strings.isEmpty(labsOngoing)}"> - <th:block th:text="${labsOngoing}"/> - <button type="button" class="close" data-dismiss="alert" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - </div> - <div class="alert alert-warning mt-md-3" role="alert" - th:unless="${#strings.isEmpty(slotSelectionOpen)}"> - <th:block th:text="${slotSelectionOpen}"/> - <button type="button" class="close" data-dismiss="alert" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - </div> <div id="calendar"/> <script th:inline="javascript"> document.addEventListener('DOMContentLoaded', function () { diff --git a/src/main/resources/templates/admin/view/running.html b/src/main/resources/templates/admin/view/running.html new file mode 100644 index 0000000000000000000000000000000000000000..aa5b60554b530896195399696d02c05e5dd8c583 --- /dev/null +++ b/src/main/resources/templates/admin/view/running.html @@ -0,0 +1,77 @@ +<!-- + + Queue - A Queueing system that can be used to handle labs in higher education + Copyright (C) 2016-2023 Delft University of Technology + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +--> +<!DOCTYPE html> +<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{admin/view}"> + +<body> +<section layout:fragment="subcontent"> + <div class="page-sub-header"> + <h3>Admin overview - Labs running today:</h3> + </div> + <div class="boxed-group" > + <div class="boxed-group-inner" th:if="${#lists.isEmpty(runningLabs)}"> + None. + </div> + + <div class="alert alert-warning mt-md-3" role="alert" + th:unless="${#strings.isEmpty(labsOngoing)}"> + <th:block th:text="${labsOngoing}"/> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="alert alert-warning mt-md-3" role="alert" + th:unless="${#strings.isEmpty(slotSelectionOpen)}"> + <th:block th:text="${slotSelectionOpen}"/> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + + <ul class="list-group"> + <li th:each="lab : ${runningLabs}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'" class="list-group-item"> + <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" + th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> + </a> + + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.open()}">Active</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.closed()}">Completed</span> + + <span class="badge badge-pill badge-danger text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> + <span class="badge badge-pill badge-warning text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> + + <span class="badge badge-pill badge-warning text-muted" + th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> + </li> + </ul> + </div> + + <!-- Invisible column for spacing at the bottom of the page --> + <div class="row mt-3"></div> +</section> +</body> +</html> diff --git a/src/main/resources/templates/edition/index.html b/src/main/resources/templates/edition/index.html index 7f2200039562cd3bd7e8d8571c91695e941a0261..f0a2ccf1b6d82ced6afc2b70ed2b24e58b4539a6 100644 --- a/src/main/resources/templates/edition/index.html +++ b/src/main/resources/templates/edition/index.html @@ -63,6 +63,17 @@ th:disabled="${!@permissionService.canCreateEdition()}"> Create new edition </a> + <button class="btn btn-outline-primary" onclick="showDialog('create-shared-edition-dialog')"> + Create course collection + </button> + <div tabindex="0" class="help-tip"> + <div class="help-icon">?</div> + <p>A course collection is a collection of courses that acts in the queue as one + course. This can be used for sessions in which multiple courses take place at + the same time. This avoids having to create a session for every + course, as sessions can be shared.</p> + </div> + <div th:replace="~{shared-edition/create/create-shared-edition :: overlay}"></div> </th:block> <h1>Course Editions</h1> </div> diff --git a/src/main/resources/templates/edition/view/status.html b/src/main/resources/templates/edition/view/status.html index 916368e57a4007b550ec8f9437066ffafc933d31..77053ed6745577fc6e63b67e063d0502c8decf8d 100644 --- a/src/main/resources/templates/edition/view/status.html +++ b/src/main/resources/templates/edition/view/status.html @@ -72,7 +72,7 @@ <div> <div id="labs" class="tab-pane"> - <div th:unless="${#lists.isEmpty(activeLabs) || #lists.isEmpty(inactiveLabs)}"> + <div th:unless="${#lists.isEmpty(activeLabs) && #lists.isEmpty(inactiveLabs)}"> <div class="row"> <form id="lab-form" method="GET" class="form-inline"> <div class="row"> @@ -219,7 +219,7 @@ </div> </div> - <div th:unless="${!#lists.isEmpty(activeLabs) && !#lists.isEmpty(inactiveLabs)}"> + <div th:if="${#lists.isEmpty(activeLabs) && #lists.isEmpty(inactiveLabs)}"> <h5>No labs</h5> </div> </div> diff --git a/src/main/resources/templates/home/dashboard.html b/src/main/resources/templates/home/dashboard.html index fec4e7393198e9c6fbbc81db07debc3b246d0793..d09211c3b153b4a6999772297c93c5f5d1d4df5b 100644 --- a/src/main/resources/templates/home/dashboard.html +++ b/src/main/resources/templates/home/dashboard.html @@ -45,19 +45,6 @@ <div class="page-header"> <h1>Home</h1> </div> - <div style="margin-bottom: 1rem" th:if="${@permissionService.isAdminOrTeacher()}"> - <button class="btn btn-outline-primary" onclick="showDialog('create-shared-edition-dialog')"> - Create course collection - </button> - <div tabindex="0" class="help-tip"> - <div class="help-icon">?</div> - <p>A course collection is a collection of courses that acts in the queue as one - course. This can be used for sessions in which multiple courses take place at - the same time. This avoids having to create a session for every - course, as sessions can be shared.</p> - </div> - <div th:replace="~{shared-edition/create/create-shared-edition :: overlay}"></div> - </div> <ul class="nav nav-tabs" id="overview-tabs" role="tablist"> <li class="nav-item"> @@ -68,10 +55,6 @@ <a class="nav-link" id="calendar-tab" data-toggle="tab" href="#calendar" role="tab" aria-controls="calendar" aria-selected="false">Calendar</a> </li> - <li class="nav-item" th:if="${@permissionService.isAdmin()}"> - <a class="nav-link" id="admin-tab" data-toggle="tab" href="#admin" role="tab" - aria-controls="admin" aria-selected="false">Admin</a> - </li> </ul> <div class="tab-content mt-2"> @@ -237,38 +220,6 @@ </div> </div> - <div class="tab-pane fade" id="admin" role="tabpanel" aria-labelledby="admin-tab" - th:if="${@permissionService.isAdmin()}"> - <div class="boxed-group" > - <h3>Admin overview - Labs running today:</h3> - <div class="boxed-group-inner" th:if="${#lists.isEmpty(runningLabs)}"> - None. - </div> - - <ul class="list-group"> - <li th:each="lab : ${runningLabs}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'" class="list-group-item"> - <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" - th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> - </a> - - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.open()}">Active</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.closed()}">Completed</span> - - <span class="badge badge-pill badge-danger text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> - <span class="badge badge-pill badge-warning text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> - - <span class="badge badge-pill badge-warning text-muted" - th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> - </li> - </ul> - </div> - </div> </div> </section> </body> diff --git a/src/main/resources/templates/lab/create/components/lab-general.html b/src/main/resources/templates/lab/create/components/lab-general.html index 4131e10f3e61dc8927ca2e74d5757d91f8221866..0f78eaccc0ebaea3c411fa76f36efc051e3631e4 100644 --- a/src/main/resources/templates/lab/create/components/lab-general.html +++ b/src/main/resources/templates/lab/create/components/lab-general.html @@ -44,7 +44,7 @@ <div class="col-sm-10"> <select class="selectpicker form-control" id="direction-select" required name="communicationMethod" th:classappend="${#fields.hasErrors('communicationMethod')} ? 'is-invalid'" - data-title="Pick a direction" onchange="updateJitsi()"> + data-title="Pick a direction"> <option th:each="cm : ${T(nl.tudelft.queue.model.enums.CommunicationMethod).values()}" th:value="${cm}" th:text="${cm.displayName}" th:selected="${dto.communicationMethod == cm}"></option> @@ -124,8 +124,8 @@ <select multiple class="selectpicker form-control" th:classappend="${#fields.hasErrors('rooms')} ? 'is-invalid'" id="room-select" th:field="*{rooms}" size="10" - data-live-search="true" required - data-title="Pick at least one room" + data-live-search="true" + data-title="Pick a room" data-actions-box="true"> <th:block th:each="room : ${rooms}"> <option th:value="${room.id}" @@ -139,6 +139,25 @@ </div> </div> + <div id="online-mode-select-div" class="form-group form-row mb-4"> + <label class="col-sm-2 col-form-label" for="online-mode-select">Online Modes</label> + <div class="col-sm-10"> + <select multiple class="selectpicker form-control" + th:classappend="${#fields.hasErrors('onlineModes')} ? 'is-invalid'" + id="online-mode-select" th:field="*{onlineModes}" size="10" + data-live-search="true" + data-title="Pick online mode(s)" + data-actions-box="true"> + <th:block th:each="onlineMode : ${T(nl.tudelft.queue.model.enums.OnlineMode).values()}"> + <option th:value="${onlineMode}" + th:selected="${dto.onlineModes.contains(onlineMode)}" + th:text="|${onlineMode.getDisplayName()}|"> + </option> + </th:block> + </select> + </div> + </div> + <div class="form-group form-row"> <label class="col-sm-2 col-form-label" for="module-select">Modules</label> <div class="col-sm-10"> @@ -255,28 +274,6 @@ $("#room-select").selectpicker("refresh"); } - function updateJitsi() { - const direction = $("#direction-select").val(); - - const buildingSelectDiv = $("#building-select-div"); - const roomSelectDiv = $("#room-select-div"); - - if (direction === "JITSI_MEET") { - buildingSelectDiv.addClass("d-none") - roomSelectDiv.addClass("d-none"); - - $("#room-select > option").each(function () { - $(this).attr("selected", $(this).val() === "94"); - }); - - $("#room-select").selectpicker("refresh"); - } else { - buildingSelectDiv.removeClass("d-none") - roomSelectDiv.removeClass("d-none"); - - updateRooms(); - } - } //]]> </script> </th:block> @@ -284,3 +281,4 @@ </body> </html> + diff --git a/src/main/resources/templates/lab/create/slotted.html b/src/main/resources/templates/lab/create/slotted.html index a32e02a55d1dd7a829854cceea9346bdb4b9379c..27a121a12106187cebb77993c731be1340cecaaf 100644 --- a/src/main/resources/templates/lab/create/slotted.html +++ b/src/main/resources/templates/lab/create/slotted.html @@ -46,7 +46,7 @@ layout:decorate="~{lab/create/regular}"> <section layout:fragment="slot-config"> <h4>Time slots</h4> <hr/> - +<!--slotted labs only--> <div class="form-group form-row"> <label for="slot-duration-input" class="col-sm-4 col-form-label">Intervals of x minutes:</label> <div class="col-sm-4"> diff --git a/src/main/resources/templates/lab/edit/components/lab-general.html b/src/main/resources/templates/lab/edit/components/lab-general.html index 8ad1e4824c44d191b5de13634767e8e1479a6bc3..cec71e4beb02da428c98b27e3bb4471e6ae6b03e 100644 --- a/src/main/resources/templates/lab/edit/components/lab-general.html +++ b/src/main/resources/templates/lab/edit/components/lab-general.html @@ -46,7 +46,7 @@ <label for="direction-select" class="col-sm-2 col-form-label">Direction:</label> <div class="col-sm-10"> <select class="selectpicker form-control" id="direction-select" - name="communicationMethod" onchange="updateJitsi()"> + name="communicationMethod"> <option th:each="cm : ${T(nl.tudelft.queue.model.enums.CommunicationMethod).values()}" th:value="${cm}" th:text="${cm.displayName}" th:selected="${lab.communicationMethod == cm}"></option> @@ -104,8 +104,7 @@ <h4>General</h4> <hr/> - <div id="building-select-div" class="form-group form-row mb-4" - th:classappend="${lab.communicationMethod.isOnline()} ? 'd-none'"> + <div id="building-select-div" class="form-group form-row mb-4"> <label class="col-sm-2 col-form-label" for="building-select">Buildings</label> <div class="col-sm-10"> <select multiple class="selectpicker form-control" size="10" @@ -123,14 +122,13 @@ </div> </div> - <div id="room-select-div" class="form-group form-row mb-4" - th:classappend="${lab.communicationMethod.isOnline()} ? 'd-none'"> + <div id="room-select-div" class="form-group form-row mb-4"> <label class="col-sm-2 col-form-label" for="room-select">Rooms</label> <div class="col-sm-10"> <select multiple class="selectpicker form-control" th:classappend="${#fields.hasErrors('rooms')} ? 'is-invalid'" id="room-select" name="rooms" size="10" - data-live-search="true" required> + data-live-search="true"> <th:block th:each="r : ${rooms}"> <option th:value="${r.id}" th:classappend="${#lists.isEmpty(lSession.rooms.?[building.id == __${r.building.id}__])} ? 'd-none'" @@ -142,6 +140,22 @@ </select> </div> </div> + <div id="online-mode-select-div" class="form-group form-row mb-4"> + <label class="col-sm-2 col-form-label" for="online-mode-select">Online Modes</label> + <div class="col-sm-10"> + <select multiple class="selectpicker form-control" + th:classappend="${#fields.hasErrors('onlineModes')} ? 'is-invalid'" + id="online-mode-select" name="onlineModes" size="10" + data-live-search="true"> + <th:block th:each="onlineMode : ${T(nl.tudelft.queue.model.enums.OnlineMode).values()}"> + <option th:value="${onlineMode}" + th:selected="${@labService.getOnlineModesInLabSession(lSession.id).contains(onlineMode)}" + th:text="|${onlineMode.getDisplayName()}|"> + </option> + </th:block> + </select> + </div> + </div> <div class="form-group form-row"> <label class="col-sm-2 col-form-label" for="module-select">Modules</label> @@ -262,29 +276,6 @@ $("#room-select").selectpicker("refresh"); } - - function updateJitsi() { - const direction = $("#direction-select").val(); - - const buildingSelectDiv = $("#building-select-div"); - const roomSelectDiv = $("#room-select-div"); - - if (direction === "JITSI_MEET") { - buildingSelectDiv.addClass("d-none") - roomSelectDiv.addClass("d-none"); - - $("#room-select > option").each(function () { - $(this).attr("selected", $(this).val() === "94"); - }); - - $("#room-select").selectpicker("refresh"); - } else { - buildingSelectDiv.removeClass("d-none") - roomSelectDiv.removeClass("d-none"); - - updateRooms(); - } - } //]]> </script> </th:block> diff --git a/src/main/resources/templates/lab/enqueue/lab.html b/src/main/resources/templates/lab/enqueue/lab.html index a6641c3c9d9fbfb7857dbe686b40ec7a744f386d..5a6ba4a4fa378fcfc836206ca1e8a07b3410d277 100644 --- a/src/main/resources/templates/lab/enqueue/lab.html +++ b/src/main/resources/templates/lab/enqueue/lab.html @@ -111,7 +111,7 @@ </div> </div> - <div class="form-group" id="room-div" th:classappend="${qSession.enableExperimental ? 'd-none' : ''}"> + <div class="form-group" id="room-div" th:unless="${#lists.isEmpty(rooms)}" th:classappend="${qSession.enableExperimental ? 'd-none' : ''}"> <label for="input-room" class="col-sm-2 control-label">Room</label> <div class="col-sm-8"> @@ -126,6 +126,26 @@ Room error </div> </div> + </div> + + <div class="form-group" id="onlineMode-div" th:unless="${#sets.isEmpty(qSession.onlineModes)}" th:classappend="${qSession.enableExperimental ? 'd-none' : ''}"> + <label for="input-onlineMode" class="col-sm-2 control-label">Online Mode</label> + + <div class="col-sm-8"> + <select class="selectpicker form-control" id="input-onlineMode" th:field="*{onlineMode}" + data-title="Pick an online mode" + th:classappend="${#fields.hasErrors('onlineMode')} ? 'is-invalid'" required> + <option th:each="onlineMode : ${qSession.onlineModes}" + th:id="|input-onlineMode-${onlineMode.name()}|" + th:value="${onlineMode}" + th:text="|${onlineMode.getDisplayName()}|"></option> + </select> + <div class="invalid-feedback" th:if="${#fields.hasErrors('onlineMode')}" th:errors="*{onlineMode}"> + Online Mode error + </div> + </div> + + </div> <div class="form-group" id="question-div" th:classappend="${qSession.enableExperimental ? 'd-none' : ''}"> @@ -182,6 +202,10 @@ const typeSelect = $("#input-type"); const questionInput = $("#input-question"); + const commentInput = $("#input-comment"); + + const roomSelect = $("#input-room"); + const onlineModeSelect = $("#input-onlineMode"); assignmentSelect.on("changed.bs.select", () => { typeSelect.val(""); @@ -220,6 +244,23 @@ } } }) + + roomSelect.on("changed.bs.select", () => { + onlineModeSelect.prop("selectedIndex",-1); + roomSelect.prop("required",true); + onlineModeSelect.prop("required",false); + commentInput.prop("disabled",false); + onlineModeSelect.selectpicker("refresh"); + }); + onlineModeSelect.on("changed.bs.select", () => { + roomSelect.prop("selectedIndex",-1); + onlineModeSelect.prop("required",true); + roomSelect.prop("required",false); + commentInput.val(''); + commentInput.prop("disabled",true); + roomSelect.selectpicker("refresh"); + }); + //]]> </script> </th:block> diff --git a/src/main/resources/templates/lab/view/components/current-request-edit.html b/src/main/resources/templates/lab/view/components/current-request-edit.html index abcd8bfeb08dc3f1f3756a5650719c46c6f05c03..042615a3f081be48037a20e6bd32657e287d5c18 100644 --- a/src/main/resources/templates/lab/view/components/current-request-edit.html +++ b/src/main/resources/templates/lab/view/components/current-request-edit.html @@ -33,31 +33,40 @@ <div class="col-12 mt-3" th:if="${current.eventInfo.status.isPending()}"> <div class="card location-info text-white"> <th:block> - <h3 class="card-header">Update your location and question</h3> + <h3 class="card-header" th:if="${current.room != null}">Update your location and question</h3> + <h3 class="card-header" th:if="${current.onlineMode != null}">Update your question</h3> <div class="card-body col-12 col-md-12"> - <span th:text="|Current room: ${current.room.building.name} - ${current.room.name}|"></span> <form class="form" method="post" th:action="@{/request/{id}/update-request-info/(id=${current.id})}" th:object="${currentDto}"> - <div class="form-group"> - <select class="custom-select custom-select-md" - th:field="*{room}"> - <option disabled value="">Choose your new room</option> - <option th:each="room : ${rooms}" - th:value="${room.id}" - th:selected="${current.room == room.id}" - th:text="|${room.building.name} - ${room.name}|"> - </option> - </select> + <div th:if="${current.room != null}" id="physical-location-info"> + <span th:text="|Current location: ${current.room?.building?.name} - ${current.room?.name}|"></span> + <div class="form-group"> + <select class="custom-select custom-select-md" + th:field="*{room}"> + <option disabled value="">Choose your new room</option> + <option th:each="room : ${rooms}" + th:value="${room.id}" + th:selected="${current.room == room.id}" + th:text="|${room.building.name} - ${room.name}|"> + </option> + </select> + </div> + + <div class="form-group" id="comment"> + <label for="inputComment">Where are you located?</label> + <input type="text" class="form-control" id="inputComment" + placeholder="Cubicle 1..." + th:field="*{comment}"/> + </div> </div> - <div class="form-group" id="comment"> - <label for="inputComment">Where are you located?</label> - <input type="text" class="form-control" id="inputComment" - placeholder="Cubicle 1..." - th:field="*{comment}"/> + <div th:if="${!#sets.isEmpty(qSession.onlineModes)}" id="online-mode-info"> + + </div> + <div th:if="${current.requestType == T(nl.tudelft.queue.model.enums.RequestType).QUESTION}" class="form-group" id="question"> <label for="inputQuestion">What is your question?</label> diff --git a/src/main/resources/templates/lab/view/components/lab-info.html b/src/main/resources/templates/lab/view/components/lab-info.html index f9f9424d5cc61a4e0c4f9a930271041dc500f1bd..6b91618b6565ffa686a3a39f40617e2c747f0f7a 100644 --- a/src/main/resources/templates/lab/view/components/lab-info.html +++ b/src/main/resources/templates/lab/view/components/lab-info.html @@ -40,7 +40,22 @@ <dd th:text="|${#temporals.format(qSession.session.start, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(qSession.session.end, 'dd MMMM yyyy HH:mm')}|"></dd> <dt>Rooms</dt> - <dd th:text="${#strings.listJoin(@roomDTOService.names(rooms), ', ')}"></dd> + <dd th:if="${#lists.isEmpty(rooms)}" + class="text-danger"> + This lab does not have any rooms + </dd> + <dd th:unless="${#lists.isEmpty(rooms)}" + th:text="${#strings.listJoin(@roomDTOService.names(rooms), ', ')}"> + </dd> + + <dt>Online Modes</dt> + <dd th:if="${#sets.isEmpty(qSession.onlineModes)}" + class="text-danger"> + This lab does not have an online alternative + </dd> + <dd th:unless="${#sets.isEmpty(qSession.onlineModes)}" + th:text="${T(nl.tudelft.queue.model.enums.OnlineMode).displayNames(qSession.onlineModes)}"> + </dd> <dt>Modules</dt> <dd th:if="${#lists.isEmpty(modules)}" diff --git a/src/main/resources/templates/request/list/filters.html b/src/main/resources/templates/request/list/filters.html index c1fb374657da598501ae748bdf019afd8fea2363..6a16cb88320760d9dd3afd8811a00c6c36e57ba3 100644 --- a/src/main/resources/templates/request/list/filters.html +++ b/src/main/resources/templates/request/list/filters.html @@ -102,6 +102,18 @@ </select> </div> + <div class="form-group"> + <label class="form-control-label" for="onlineMode-select">Online Mode</label> + <select multiple class="form-control selectpicker" id="onlineMode-select" th:field="*{onlineModes}"> + <th:block th:each="onlineMode : ${T(nl.tudelft.queue.model.enums.OnlineMode).values()}"> + <option th:value="${onlineMode}" + th:selected="${filter.onlineModes.contains(onlineMode)}" + th:text="|${onlineMode.displayName}|"> + </option> + </th:block> + </select> + </div> + <div class="form-group"> <label class="form-control-label" for="assigned-select">Assigned</label> <select multiple class="form-control selectpicker" id="assigned-select" diff --git a/src/main/resources/templates/request/list/request-table.html b/src/main/resources/templates/request/list/request-table.html index 03d362f4a254cc7ba2d570031bc124c2112ef430..09b08492383bedfb68db2539c4cd8a4daf1a3246 100644 --- a/src/main/resources/templates/request/list/request-table.html +++ b/src/main/resources/templates/request/list/request-table.html @@ -66,10 +66,14 @@ th:text="${request.requesterEntityName()}"> </a> </td> - <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> + <td th:if="${request.room != null && request.room.building != null}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> <a class="text-white" th:href="@{/request/{id}(id=${request.id})}" th:text="|${request.room.building.name} - ${request.room.name}|"></a> </td> + <td th:if="${request.onlineMode != null}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> + <a class="text-white" th:href="@{/request/{id}(id=${request.id})}" + th:text="|${'@Online'} - ${request.onlineMode.displayName}|"></a> + </td> <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> <a class="text-white" th:href="@{/request/{id}(id=${request.id})}" th:text="|${request.assignment.name} (${request.assignment.module.name})|"> @@ -105,7 +109,13 @@ <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><span class="badge badge-pill bg-danger" id="status-{{id}}">NEW</span></td> <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><a th:href="${base + '/{{id}}'}" class="text-white">{{requestTypeDisplayName}}</a></td> <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><a th:href="${base + '/{{id}}'}" class="text-white">{{requestedBy}}</a></td> - <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><a th:href="${base + '/{{id}}'}" class="text-white">{{buildingName}} - {{roomName}}</a></td> + {{#if roomName}} + {{#if buildingName}} + <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><a th:href="${base + '/{{id}}'}" class="text-white">{{buildingName}} - {{roomName}}</a></td> + {{/if}} + {{else if onlineMode}} + <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><a th:href="${base + '/{{id}}'}" class="text-white">@Online - {{onlineModeDisplayName}}</a></td> + {{/if}} <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><a th:href="${base + '/{{id}}'}" class="text-white">{{assignmentName}} ({{moduleName}})</a><br/> <small>Right now</small> </td> diff --git a/src/main/resources/templates/request/view.html b/src/main/resources/templates/request/view.html index 17d7c98fb21b121cb31d8bebc4a88a2b0b543a48..e07c580e79b5f7c0eb6cf42d408e84132f1e9851 100644 --- a/src/main/resources/templates/request/view.html +++ b/src/main/resources/templates/request/view.html @@ -65,7 +65,7 @@ <script src="/js/map_loader.js"></script> <script type="text/javascript" th:inline="javascript"> - const roomId = /*[[${request.room.id}]]*/ 0; + const roomId = /*[[${request.room != null} ? ${request.room.id}]]*/ 0; updateRequestInfo(roomId); </script> diff --git a/src/main/resources/templates/request/view/components/lab-request-info.html b/src/main/resources/templates/request/view/components/lab-request-info.html index 4229f7f7f9f9931c5ba63fde544a552438b024b7..fe5d9419c7cb8d28730ce5e75f2bef7cd587eba5 100644 --- a/src/main/resources/templates/request/view/components/lab-request-info.html +++ b/src/main/resources/templates/request/view/components/lab-request-info.html @@ -49,8 +49,16 @@ <dt>Assignment</dt> <dd th:text="${request.assignment.name}"></dd> - <dt>Room</dt> - <dd th:text="|${request.room.building.name} - ${request.room.name}|"></dd> + <th:block th:if="${request.room != null}"> + <dt>Room</dt> + <dd th:text="|${request.room.building.name} - ${request.room.name}|"></dd> + </th:block> + + <th:block th:if="${request.onlineMode != null}"> + <dt>Online Mode</dt> + <dd th:text="|${'@Online'} - ${request.onlineMode.displayName}|"></dd> + </th:block> + <dt>Type</dt> <dd th:text="${request.requestType.displayName()}"></dd> @@ -65,7 +73,8 @@ <dd th:text="${request.comment}"></dd> </th:block> - <th:block th:if="${request.getJitsiRoom() != null && @permissionService.canViewRequestJitsiRoom(request.id)}"> + <!--TODO This block needs to be changed to support other online modes --> + <th:block th:if="${request.getOnlineMode() == T(nl.tudelft.queue.model.enums.OnlineMode).JITSI && request.getJitsiRoom() != null && @permissionService.canViewRequestJitsiRoom(request.id)}"> <dt>Link to Jitsi Room</dt> <dd> <a th:href="@{${@jitsiService.getJitsiRoomUrl(request.data)}}" th:target="_blank" diff --git a/src/main/resources/templates/shared-edition/view/session-list.html b/src/main/resources/templates/shared-edition/view/session-list.html index f5f31fbd8d5bb92c2bb93e7088682a9249e25674..ec18c69fcb07d58a6e293e993236877963895f36 100644 --- a/src/main/resources/templates/shared-edition/view/session-list.html +++ b/src/main/resources/templates/shared-edition/view/session-list.html @@ -53,9 +53,33 @@ <td> <a th:text="${lab.name}" th:href="@{/lab/{id}(id=${lab.id})}"></a> <span th:if="${lab.isShared}" class="badge badge-pill badge-info text-white">Shared lab</span> + </td> <td th:text="${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')}"></td> - <td th:text="${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')}"></td> + <td> + <span th:text="${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')}"></span> + <div class="btn-group float-right" th:if="${@permissionService.canManageInAnyEdition(lab.associatedEditions)}"> + + <th:block> + <a th:href="@{/lab/{id}/edit(id=${lab.id})}" class="btn btn-sm btn-secondary"> + <i class="fa fa-pencil" aria-hidden="true"></i> + </a> + </th:block> + <th:block> + <a th:href="@{/lab/{id}/remove(id=${lab.id})}" + class="btn btn-sm btn-danger"> + <i class="fa fa-trash-o" aria-hidden="true"></i> + </a> + </th:block> + <th:block> + <a href="#" + th:href="@{/lab/{labId}/copy(labId=${lab.id})}" + class="btn btn-sm btn-primary"> + <i class="fa fa-copy" aria-hidden="true"></i> + </a> + </th:block> + </div> + </td> </tr> </th:block> </div> diff --git a/src/test/java/nl/tudelft/queue/controller/AdminControllerTest.java b/src/test/java/nl/tudelft/queue/controller/AdminControllerTest.java index 5c35c2ee265350820afd5c9d2ca12a4fd0af5e00..b6d780e667521ae4cbd39e7513eaa5fd7d22e664 100644 --- a/src/test/java/nl/tudelft/queue/controller/AdminControllerTest.java +++ b/src/test/java/nl/tudelft/queue/controller/AdminControllerTest.java @@ -17,24 +17,18 @@ */ package nl.tudelft.queue.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import javax.transaction.Transactional; import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.dto.EditionDetailsDTO; -import nl.tudelft.labracore.api.dto.EditionSummaryDTO; import nl.tudelft.queue.service.LabService; import nl.tudelft.queue.service.PermissionService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -42,7 +36,6 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.web.servlet.MockMvc; -import reactor.core.publisher.Flux; import test.TestDatabaseLoader; import test.labracore.EditionApiMocker; import test.test.TestQueueApplication; @@ -82,12 +75,7 @@ public class AdminControllerTest { @Test @WithUserDetails("admin") void getAdminPanel() throws Exception { - when(ls.countOngoingLabs(anyList())).thenReturn(1L); - - mockMvc.perform(get("/admin/calendar")).andExpect(status().isOk()).andExpect(model().attribute( - "labsOngoing", "Currently 1 labs ongoing!")).andReturn(); - when(eApi.getAllEditionsActiveDuringPeriod(any())) - .thenReturn(Flux.just(new ModelMapper().map(oopNow, EditionSummaryDTO.class))); + mockMvc.perform(get("/admin/running")).andExpect(status().isOk()); } } diff --git a/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java b/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java index 89034a3c02bbc47a959e3dff49b2debdc3e82f19..ef4a3b56b8acc29ce41d945faef092d2ff934174 100644 --- a/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java +++ b/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java @@ -56,6 +56,7 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import test.TestDatabaseLoader; import test.labracore.*; @@ -83,6 +84,12 @@ public class EditionControllerTest { @Autowired private ModuleApiMocker mApiMocker; + @Autowired + private EditionCollectionApiMocker ecApiMocker; + + @Autowired + private PersonApiMocker pApiMocker; + @Autowired private RoleApiMocker rApiMocker; @@ -107,6 +114,8 @@ public class EditionControllerTest { rApiMocker.mock(); sApiMocker.mock(); sgApiMocker.mock(); + ecApiMocker.mock(); + pApiMocker.mock(); student100 = db.getStudents()[100]; student155 = db.getStudents()[155]; @@ -122,15 +131,20 @@ public class EditionControllerTest { when(eApi.getEditionsPageActiveOrTaughtBy(any(), any(), any(), any())) .thenReturn(Mono.just(new PageEditionDetailsDTO().content(List.of()).totalElements(0L))); + when(eApi.getAllEditionsActiveDuringPeriod(any())).thenReturn(Flux.just()); + mvc.perform(get("/editions")) .andExpect(status().isOk()) .andExpect(model().attribute("page", "catalog")) - .andExpect(model().attributeExists("editions")); + .andExpect(model().attributeExists("editions")) + .andExpect(model().attributeExists("allEditions")); } @Test @WithUserDetails("teacher0") void submitFiltersRedirectsBackToEditions() throws Exception { + when(eApi.getAllEditionsActiveDuringPeriod(any())).thenReturn(Flux.just()); + MockHttpSession session = new MockHttpSession(); mvc.perform(post("/editions/filter").with(csrf()) @@ -146,6 +160,8 @@ public class EditionControllerTest { when(eApi.getEditionsPageActiveOrTaughtBy(any(), any(), any(), any())) .thenReturn(Mono.just(new PageEditionDetailsDTO().content(List.of()).totalElements(0L))); + when(eApi.getAllEditionsActiveDuringPeriod(any())).thenReturn(Flux.just()); + MockHttpSession session = new MockHttpSession(); mvc.perform(post("/editions/filter").with(csrf()) @@ -167,6 +183,21 @@ public class EditionControllerTest { .andExpect(view().name("course/request")); } + @Test + @WithUserDetails("student155") + void allEditionsNotExposedToStudent() throws Exception { + when(eApi.getEditionsPageActiveOrTaughtBy(any(), any(), any(), any())) + .thenReturn(Mono.just(new PageEditionDetailsDTO().content(List.of()).totalElements(0L))); + + when(eApi.getAllEditionsActiveDuringPeriod(any())).thenReturn(Flux.just()); + + mvc.perform(get("/editions")) + .andExpect(status().isOk()) + .andExpect(model().attribute("page", "catalog")) + .andExpect(model().attributeExists("editions")) + .andExpect(model().attributeDoesNotExist("allEditions")); + } + @Test @WithUserDetails("student155") void getEditionEnrolViewContainsEdition() throws Exception { diff --git a/src/test/java/nl/tudelft/queue/controller/HomeControllerTest.java b/src/test/java/nl/tudelft/queue/controller/HomeControllerTest.java index d5be0a02360615bd0c432d1396de8a1bac7a14d1..c97a354986a3c257f8d0e8eadd00ded55a242dab 100644 --- a/src/test/java/nl/tudelft/queue/controller/HomeControllerTest.java +++ b/src/test/java/nl/tudelft/queue/controller/HomeControllerTest.java @@ -126,9 +126,8 @@ class HomeControllerTest { mvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(model().attribute("user", matchPersonId(student50.getId()))) - .andExpect(model().attributeExists("activeRoles", "editions")) - .andExpect(model().attributeDoesNotExist("runningEditions")) - .andExpect(model().attributeDoesNotExist("sharedEdition")) + .andExpect(model().attributeExists("activeRoles", "editions", "labs")) + .andExpect(model().attributeExists("sharedEditions", "sharedLabs")) .andExpect(view().name("home/dashboard")); verify(personApi).getPersonById(student50.getId()); @@ -148,8 +147,6 @@ class HomeControllerTest { .andExpect(model().attribute("user", matchPersonId(admin.getId()))) .andExpect(model().attribute("activeRoles", List.of())) .andExpect(model().attribute("editions", Map.of())) - .andExpect(model().attribute("runningLabs", List.of())) - .andExpect(model().attributeExists("allEditions")) .andExpect(view().name("home/dashboard")); verify(personApi).getPersonById(admin.getId()); @@ -163,7 +160,6 @@ class HomeControllerTest { mvc.perform(get("/")) .andExpect(status().isOk()) - .andExpect(model().attributeExists("allEditions")) .andExpect(view().name("home/dashboard")); } diff --git a/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java b/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java index c487fc419ec87a5eb069490570523165f54c52cf..0bf830f5bbed3c3a9ef921b6abaa445eb0fe6c2f 100644 --- a/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java +++ b/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java @@ -42,8 +42,10 @@ import nl.tudelft.queue.dto.patch.labs.ExamLabPatchDTO; import nl.tudelft.queue.dto.patch.labs.RegularLabPatchDTO; import nl.tudelft.queue.dto.patch.labs.SlottedLabPatchDTO; import nl.tudelft.queue.model.LabRequest; +import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.SelectionRequest; import nl.tudelft.queue.model.embeddables.*; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.events.RequestApprovedEvent; @@ -51,6 +53,7 @@ import nl.tudelft.queue.model.events.RequestForwardedToAnyEvent; import nl.tudelft.queue.model.events.RequestTakenEvent; import nl.tudelft.queue.model.labs.*; import nl.tudelft.queue.repository.*; +import nl.tudelft.queue.service.JitsiService; import nl.tudelft.queue.service.LabService; import nl.tudelft.queue.service.RequestService; @@ -59,6 +62,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -82,7 +87,7 @@ class LabControllerTest { @Autowired private MockMvc mvc; - @Autowired + @SpyBean private QueueSessionRepository qsr; @SpyBean @@ -91,6 +96,9 @@ class LabControllerTest { @SpyBean private RequestService rs; + @SpyBean + private JitsiService js; + @Autowired private RequestRepository rr; @@ -106,6 +114,9 @@ class LabControllerTest { @Autowired private SessionControllerApi sApi; + @Captor + private ArgumentCaptor<QueueSession<LabRequest>> queueSessionArgumentCaptor; + private PersonSummaryDTO teacher1; private PersonSummaryDTO student5; @@ -121,6 +132,8 @@ class LabControllerTest { private Lab regLab1; private Lab expLab1; + + private Lab regHybridLab1; private SlottedLab slottedLab1; private ExamLab examLabNow; private Lab sharedLab; @@ -156,6 +169,7 @@ class LabControllerTest { slottedLab1 = slr.getById(db.getOopNowSlottedLab1().getId()); examLabNow = elr.getById(db.getOopExamNow().getId()); sharedLab = db.getRlOopNowSharedLab(); + regHybridLab1 = db.getOopNowRegularHybridLab1(); pastLecture = db.getOopPastLecture(); qSession2 = db.getOopLectureRandom(); @@ -331,6 +345,35 @@ class LabControllerTest { .build()), eq(student5.getId()), anyBoolean()); } + @Test + @WithUserDetails("student5") + void enqueWithOnlineModeSelectedWorks() throws Exception { + mvc.perform(post("/lab/" + regHybridLab1.getId() + "/enqueue/lab").with(csrf()) + .queryParam("requestType", "QUESTION") + .queryParam("question", "This is the question I am asking??????") + .queryParam("assignment", "" + assignment1.getId()) + .queryParam("onlineMode", "JITSI")) + .andExpect(redirectedUrl("/lab/" + regHybridLab1.getId())); + + verify(js, times(1)).createJitsiRoomName(any()); + + } + + @Test + @WithUserDetails("student5") + void enqueHybridLabWithRoomAndOnlineModeShouldFail() throws Exception { + mvc.perform(post("/lab/" + regHybridLab1.getId() + "/enqueue/lab").with(csrf()) + .queryParam("requestType", "QUESTION") + .queryParam("question", "This is the question I am asking??????") + .queryParam("assignment", "" + assignment1.getId()) + .queryParam("room", "" + room1.getId()) + .queryParam("onlineMode", "JITSI")) + .andExpect(status().is4xxClientError()); + + verify(js, never()).createJitsiRoomName(any()); + + } + @Test @WithUserDetails("student5") void processingRequestForLabForwardsToRequests() throws Exception { @@ -544,6 +587,16 @@ class LabControllerTest { verify(ls, times(1)).deleteSession(any(Lab.class)); } + @Test + @WithUserDetails("admin") + void sharedLabDeleteRedirectsToSharedLabPage() throws Exception { + mvc.perform(post("/lab/{id}/remove", sharedLab.getId()).with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlTemplate("/shared-edition/{shared-edition-id}", ec1.getId())); + + verify(ls, times(1)).deleteSession(any(Lab.class)); + } + @Test @WithUserDetails("admin") void getLabCopyViewWorks() throws Exception { @@ -608,6 +661,54 @@ class LabControllerTest { verify(ls).updateSession(isA(ExamLabPatchDTO.class), isA(ExamLab.class)); } + @Test + @WithUserDetails("admin") + void sessionWithOnlineModesOnlySucceeds() throws Exception { + + mvc.perform(regularPatch(post("/lab/" + regLab1.getId() + "/edit/regular").with(csrf())) + .queryParam("onlineModes", "JITSI")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/lab/" + regLab1.getId())); + + verify(ls).updateSession(isA(RegularLabPatchDTO.class), isA(RegularLab.class)); + } + + @Test + @WithUserDetails("admin") + void createHybridLabWorksNormal() throws Exception { + + mvc.perform(regularCreate(post("/edition/" + oopNow.getId() + "/lab/create/regular")) + .queryParam("onlineModes", "JITSI").with(csrf())) + .andExpect(status().is3xxRedirection()); + + verify(ls).createSessions(isA(RegularLabCreateDTO.class), any(), eq(LabService.SessionType.REGULAR)); + + verify(qsr, atLeastOnce()).save(queueSessionArgumentCaptor.capture()); + List<QueueSession<LabRequest>> qsList = queueSessionArgumentCaptor.getAllValues(); + var result = qsList.stream().filter(x -> x instanceof Lab).map(x -> (Lab) x) + .filter(x -> x.getOnlineModes().size() != 0).findFirst(); + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getOnlineModes()).containsExactlyInAnyOrder(OnlineMode.JITSI); + } + + @Test + @WithUserDetails("admin") + void createInvalidNoRoomNoOnlineModeLabShouldFail() throws Exception { + mvc.perform(invalidRegularCreate(post("/edition/" + oopNow.getId() + "/lab/create/regular"))) + .andExpect(status().is4xxClientError()); + } + + @Test + @WithUserDetails("admin") + void createLabWithOnlyOnlineWorks() throws Exception { + mvc.perform(invalidRegularCreate(post("/edition/" + oopNow.getId() + "/lab/create/regular")) + .queryParam("onlineModes", "JITSI").with(csrf())) + .andExpect(status().is3xxRedirection()); + + verify(ls).createSessions(isA(RegularLabCreateDTO.class), any(), eq(LabService.SessionType.REGULAR)); + + } + @Test @WithUserDetails("student150") void redirectToEnrollPageWhenNotEnrolled() throws Exception { @@ -709,4 +810,14 @@ class LabControllerTest { .queryParam("rooms", "" + room1.getId()) .queryParam("eolGracePeriod", "15"); } + + private MockHttpServletRequestBuilder invalidRegularCreate(MockHttpServletRequestBuilder req) { + return req.queryParam("name", "Super Lab") + .queryParam("slot.opensAt", "09-09-2020 12:09") + .queryParam("slot.closesAt", "10-09-2020 12:09") + .queryParam("communicationMethod", "STUDENT_VISIT_TA") + .queryParam("modules", "" + labModule.getId()) + .queryParam("requestTypes['" + assignment1.getId() + "']", "QUESTION") + .queryParam("eolGracePeriod", "15"); + } } diff --git a/src/test/java/nl/tudelft/queue/controller/SharedEditionControllerTest.java b/src/test/java/nl/tudelft/queue/controller/SharedEditionControllerTest.java index b234fbd92b2f30b291292b33646092ae4a276b75..d57f92fa3c64c7555fb02b71c311952496a74093 100644 --- a/src/test/java/nl/tudelft/queue/controller/SharedEditionControllerTest.java +++ b/src/test/java/nl/tudelft/queue/controller/SharedEditionControllerTest.java @@ -46,6 +46,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -59,6 +60,7 @@ import test.test.TestQueueApplication; @Transactional @AutoConfigureMockMvc +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) @SpringBootTest(classes = TestQueueApplication.class) public class SharedEditionControllerTest { diff --git a/src/test/java/nl/tudelft/queue/model/enums/OnlineModeTest.java b/src/test/java/nl/tudelft/queue/model/enums/OnlineModeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f8af5353103edc12831a9f34519d43a8276f1626 --- /dev/null +++ b/src/test/java/nl/tudelft/queue/model/enums/OnlineModeTest.java @@ -0,0 +1,41 @@ +/* + * Queue - A Queueing system that can be used to handle labs in higher education + * Copyright (C) 2016-2021 Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.queue.model.enums; + +import static nl.tudelft.queue.model.enums.OnlineMode.*; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class OnlineModeTest { + + @Test + void testDisplayNames() { + assertThat(OnlineMode.displayNames(Set.of(JITSI))).isEqualTo("Jitsi"); + } + + @ParameterizedTest + @EnumSource(OnlineMode.class) + void displayNameIsCapitalized(OnlineMode mode) { + assertThat(mode.getDisplayName().charAt(0)).isUpperCase(); + } +} diff --git a/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java b/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java index 615dac96a298ff92aedcad0ac8977eafbe3dd4af..7dd15cdc9113277fcab99a7e93bd3301bc02ae05 100644 --- a/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java @@ -121,13 +121,14 @@ public class EditionServiceTest { LocalDateTime labTime = now(); labSlot = new Slot(labTime.minusMinutes(5), labTime.plusMinutes(10)); - regularLab = new QueueSessionSummaryDTO(0L, REGULAR, "lab", "ReadableName", labSlot, 0, "", + regularLab = new QueueSessionSummaryDTO(0L, REGULAR, null, "lab", "ReadableName", labSlot, 0, "", false, false, QueueSessionStatus.OPEN, false); - examLab = new QueueSessionSummaryDTO(1L, EXAM, "lab", "ReadableName", labSlot, 0, "", + examLab = new QueueSessionSummaryDTO(1L, EXAM, null, "lab", "ReadableName", labSlot, 0, "", false, false, QueueSessionStatus.OPEN, false); - slottedLab = new QueueSessionSummaryDTO(2L, SLOTTED, "lab", "ReadableName", labSlot, 0, "", false, + slottedLab = new QueueSessionSummaryDTO(2L, SLOTTED, null, "lab", "ReadableName", labSlot, 0, "", + false, false, QueueSessionStatus.OPEN, false); @@ -170,11 +171,13 @@ public class EditionServiceTest { void sortLabsSortsOpenBeforeClosed() { QueueSessionSummaryDTO openLab = new QueueSessionSummaryDTO(0L, REGULAR, + null, "lab", "ReadableName", new Slot(now().minusMinutes(5), now().plusMinutes(10)), 0, "", false, false, QueueSessionStatus.OPEN, false); QueueSessionSummaryDTO closedLab = new QueueSessionSummaryDTO(0L, REGULAR, + null, "lab", "ReadableName", new Slot(now().minusMinutes(10), now().minusMinutes(5)), 0, "", false, false, QueueSessionStatus.OPEN, false); @@ -187,11 +190,13 @@ public class EditionServiceTest { void sortLabsSortsPastLabsAfterFuture() { QueueSessionSummaryDTO pastLab = new QueueSessionSummaryDTO(0L, REGULAR, + null, "lab", "ReadableName", new Slot(now().minusMinutes(15), now().minusMinutes(10)), 0, "", false, false, QueueSessionStatus.OPEN, false); QueueSessionSummaryDTO futureLab = new QueueSessionSummaryDTO(0L, REGULAR, + null, "lab", "ReadableName", new Slot(now().plusMinutes(20), now().plusMinutes(100)), 0, "", false, false, QueueSessionStatus.OPEN, false); @@ -204,16 +209,19 @@ public class EditionServiceTest { void sortLabsSortsOnMinutes() { QueueSessionSummaryDTO lab1 = new QueueSessionSummaryDTO(0L, REGULAR, + null, "lab", "ReadableName", new Slot(now().plusMinutes(5), now().plusMinutes(50)), 0, "", false, false, QueueSessionStatus.OPEN, false); QueueSessionSummaryDTO lab2 = new QueueSessionSummaryDTO(0L, REGULAR, + null, "lab", "ReadableName", new Slot(now().plusMinutes(10), now().plusMinutes(15)), 0, "", false, false, QueueSessionStatus.OPEN, false); QueueSessionSummaryDTO lab3 = new QueueSessionSummaryDTO(0L, REGULAR, + null, "lab", "ReadableName", new Slot(now().plusMinutes(70), now().plusMinutes(90)), 0, "", false, false, QueueSessionStatus.OPEN, false); @@ -225,16 +233,19 @@ public class EditionServiceTest { void sortLabsSortsOnName() { QueueSessionSummaryDTO lab1 = new QueueSessionSummaryDTO(0L, REGULAR, + null, "Alab", "ReadableName", labSlot, 0, "", false, false, QueueSessionStatus.OPEN, false); QueueSessionSummaryDTO lab2 = new QueueSessionSummaryDTO(0L, REGULAR, + null, "Flab", "ReadableName", labSlot, 0, "", false, false, QueueSessionStatus.OPEN, false); QueueSessionSummaryDTO lab3 = new QueueSessionSummaryDTO(0L, REGULAR, + null, "Zlab", "ReadableName", labSlot, 0, "", false, false, QueueSessionStatus.OPEN, false); diff --git a/src/test/java/nl/tudelft/queue/service/LabServiceTest.java b/src/test/java/nl/tudelft/queue/service/LabServiceTest.java index aca75ef83a729c7b2ceaf0f80512f8f244fcedb1..2efe779fccbe3a1a0d2f9fd97b04eab55113e471 100644 --- a/src/test/java/nl/tudelft/queue/service/LabServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/LabServiceTest.java @@ -49,6 +49,7 @@ import nl.tudelft.queue.model.embeddables.AllowedRequest; import nl.tudelft.queue.model.embeddables.Slot; import nl.tudelft.queue.model.embeddables.SlottedLabConfig; import nl.tudelft.queue.model.enums.CommunicationMethod; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.enums.SelectionProcedure; import nl.tudelft.queue.model.labs.CapacitySession; @@ -124,6 +125,8 @@ class LabServiceTest { private LabCreateDTO<RegularLab> createDTO; private LabPatchDTO<RegularLab> labPatchDTO; + private LabPatchDTO<RegularLab> addOnlineModesPatchDTO; + private Model model; @BeforeEach @@ -145,7 +148,7 @@ class LabServiceTest { room1 = db.getRoomPc1(); lab1 = RegularLab.builder() - .communicationMethod(CommunicationMethod.JITSI_MEET) + .communicationMethod(CommunicationMethod.STUDENT_VISIT_TA) .session(session1.getId()) .allowedRequests(Set.of(new AllowedRequest(assignment1.getId(), RequestType.QUESTION))) .build(); @@ -153,13 +156,13 @@ class LabServiceTest { .slottedLabConfig(SlottedLabConfig.builder() .selectionOpensAt(LocalDateTime.now().minusDays(1L)) .build()) - .communicationMethod(CommunicationMethod.JITSI_MEET) + .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) .session(session2.getId()) .allowedRequests(Set.of(new AllowedRequest(assignment1.getId(), RequestType.QUESTION))) .build(); createDTO = RegularLabCreateDTO.builder() - .communicationMethod(CommunicationMethod.JITSI_MEET) + .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) .modules(Set.of(module1.getId())) .name("Lab 1") .eolGracePeriod(15) @@ -182,6 +185,8 @@ class LabServiceTest { .closesAt(LocalDateTime.now().plusHours(1)) .build()) .build(); + addOnlineModesPatchDTO = RegularLabPatchDTO.builder() + .onlineModes(Set.of(OnlineMode.JITSI)).build(); } @Test @@ -419,6 +424,15 @@ class LabServiceTest { assertThat(lab1.getCommunicationMethod()).isEqualTo(CommunicationMethod.STUDENT_VISIT_TA); } + @Test + void updateLabToOnlyhaveOnlineModesWorks() { + when(sApi.patchSession(any(), any())).thenReturn(Mono.empty()); + + ls.updateSession(addOnlineModesPatchDTO, lab1); + + assertThat(lab1.getOnlineModes()).containsExactly(OnlineMode.JITSI); + } + @Test void updateLabIncludesAssignmentsIfNonEmpty() { when(sApi.patchSession(any(), any())).thenReturn(Mono.empty()); @@ -456,4 +470,11 @@ class LabServiceTest { ls.deleteSession(lab1); assertThat(lab1.getDeletedAt()).isNotNull(); } + + @Test + void getOnlineModesInLabSession() { + var testLab = db.getOopNowRegularHybridLab1(); + var result = ls.getOnlineModesInLabSession(testLab.getSession()); + assertThat(result).containsExactlyInAnyOrder(OnlineMode.JITSI); + } } diff --git a/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java b/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java index 50d0e05c8b7bc223180b33ac1c5caf6ae87681f3..597f8f8c5670829f720f16e85c60b45bf228141f 100644 --- a/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java @@ -29,10 +29,14 @@ import java.util.stream.Collectors; import javax.transaction.Transactional; +import nl.tudelft.labracore.api.AssignmentControllerApi; +import nl.tudelft.labracore.api.RoleControllerApi; import nl.tudelft.labracore.api.StudentGroupControllerApi; import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.dto.create.requests.SelectionRequestCreateDTO; +import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.model.QSelectionRequest; import nl.tudelft.queue.model.SelectionRequest; import nl.tudelft.queue.model.embeddables.CapacitySessionConfig; @@ -40,6 +44,7 @@ import nl.tudelft.queue.model.embeddables.LabRequestConstraints; import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.enums.SelectionProcedure; import nl.tudelft.queue.model.labs.CapacitySession; +import nl.tudelft.queue.model.labs.RegularLab; import nl.tudelft.queue.repository.CapacitySessionRepository; import nl.tudelft.queue.repository.SelectionRequestRepository; @@ -47,11 +52,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.annotation.DirtiesContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import test.TestDatabaseLoader; import test.labracore.*; import test.test.TestQueueApplication; @@ -89,9 +97,17 @@ public class RequestServiceTest { private CapacitySession session; + private RegularLab oopNowRegularLab1; + + private RoleDetailsDTO[] oopNowTAs; + + private LabRequest[] rlOopNowSharedLabRequests; + CapacitySessionConfig.CapacitySessionConfigBuilder sessionConfigBuilderSelectNow; CapacitySession.CapacitySessionBuilder<?, ? extends CapacitySession.CapacitySessionBuilder<?, ?>> sessionBuilder; + @Autowired + private TestDatabaseLoader db; @Autowired private CapacitySessionRepository csr; @@ -116,12 +132,24 @@ public class RequestServiceTest { @Autowired private PersonApiMocker pApiMocker; + @Autowired + private RoleApiMocker rlApiMocker; + @Autowired private ModuleApiMocker mApiMocker; + @Autowired + private AssignmentApiMocker asApiMocker; + + @SpyBean + private AssignmentControllerApi asApi; + @Autowired private StudentGroupControllerApi sgApi; + @Autowired + private RoleControllerApi rlApi; + @BeforeEach void setUp() { sessionBuilder = CapacitySession.builder() @@ -134,6 +162,12 @@ public class RequestServiceTest { .enrolmentClosesAt(LocalDateTime.now().minusDays(1)) .selectionAt(LocalDateTime.now().minusMinutes(3)); + oopNowRegularLab1 = db.getOopNowRegularLab1(); + + oopNowTAs = db.getOopNowTAs(); + + rlOopNowSharedLabRequests = db.getRlOopNowSharedLabRequests(); + sApiMocker.save(lcSessionNow); sApiMocker.save(lcSessionOld1); sApiMocker.save(lcSessionOld2); @@ -143,8 +177,10 @@ public class RequestServiceTest { sApiMocker.mock(); sgApiMocker.mock(); rApiMocker.mock(); + rlApiMocker.mock(); pApiMocker.mock(); mApiMocker.mock(); + asApiMocker.mock(); } private void mockStudentGroup(Long moduleId, List<Long> requesterIds, Long studentGroupId) { @@ -304,4 +340,22 @@ public class RequestServiceTest { verify(sgApi).getStudentGroupsById(List.of(6L)); verify(sgApi).addGroup(any()); } + + @Test + void sharedEditionFilterDoesNotCallApiWhenEmptyRequests() { + assertThat(rs.filterRequestsSharedEditionCheck(new ArrayList<>(), new Person())).isEmpty(); + + verify(asApi, never()).getAssignmentsWithModules(any()); + + } + + @Test + @WithUserDetails("student200") + void oopTaGetsOopRequestsOnlyInSharedSession() { + Person p1 = new Person(); + p1.setId(oopNowTAs[0].getPerson().getId()); + assertThat(rs.filterRequestsSharedEditionCheck(Arrays.stream(rlOopNowSharedLabRequests).toList(), p1)) + .containsExactly(rlOopNowSharedLabRequests); + } + } diff --git a/src/test/java/nl/tudelft/queue/service/WebSocketServiceTest.java b/src/test/java/nl/tudelft/queue/service/WebSocketServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7e0821c1450b0c1f7f81de16b8f648cbc3115464 --- /dev/null +++ b/src/test/java/nl/tudelft/queue/service/WebSocketServiceTest.java @@ -0,0 +1,132 @@ +/* + * Queue - A Queueing system that can be used to handle labs in higher education + * Copyright (C) 2016-2021 Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.queue.service; + +import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*; +import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.TA; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Set; + +import javax.transaction.Transactional; + +import nl.tudelft.labracore.api.AssignmentControllerApi; +import nl.tudelft.labracore.api.EditionControllerApi; +import nl.tudelft.labracore.api.SessionControllerApi; +import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.queue.model.LabRequest; +import nl.tudelft.queue.model.enums.RequestType; +import nl.tudelft.queue.model.labs.RegularLab; +import nl.tudelft.queue.realtime.messages.RequestCreatedMessage; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import reactor.core.publisher.Mono; +import test.TestDatabaseLoader; +import test.test.TestQueueApplication; + +@Transactional +@SpringBootTest(classes = TestQueueApplication.class) +class WebSocketServiceTest { + + @Autowired + private TestDatabaseLoader db; + @SpyBean + private WebSocketService wss; + + @Autowired + private AssignmentControllerApi aApi; + + @Autowired + private EditionControllerApi eApi; + + @Autowired + private SessionControllerApi sApi; + + private EditionDetailsDTO oopNow; + + @Captor + private ArgumentCaptor<List<PersonSummaryDTO>> peopleCaptor; + + RegularLab rlOopNowSharedLab; + + AssignmentDetailsDTO oopAssignment1; + + LabRequest sharedLabRequest; + + AssignmentModuleDetailsDTO oopAssignment1ModuleDetails; + + @BeforeEach + void setup() { + + db.mockAll(); + + oopNow = db.getOopNow(); + oopAssignment1 = db.getOopNowAssignments()[0]; + rlOopNowSharedLab = db.getRlOopNowSharedLab(); + + sharedLabRequest = LabRequest.builder() + .requestType(RequestType.SUBMISSION) + .assignment(oopAssignment1.getId()) + .question("Sample Question asdfasfsadfsa") + .room(db.getRoomCz1().getId()) + .session(db.getRlOopNowSharedLab()) + .requester(db.getOopNowStudents()[0].getId().getPersonId()) + .build(); + + oopAssignment1ModuleDetails = new AssignmentModuleDetailsDTO() + .id(oopAssignment1.getId()) + .module(new ModuleLayer1DTO() + .edition(new EditionSummaryDTO().id(db.getOopNow().getId()))); + + } + + @Test + void websocketMessageGetsSentToCorrectPeople() { + var oopImportantPeople = eApi.getEditionParticipants(oopNow.getId()) + .collectList() + .block() + .stream() + .filter(r -> r != null && Set.of(TEACHER, TEACHER_RO, HEAD_TA, TA).contains(r.getType())) + .map(RolePersonDetailsDTO::getPerson) + .distinct() + .toList(); + + var apiReturn = Mono.just(oopAssignment1ModuleDetails); + when(aApi.getAssignmentById(any())).thenReturn(apiReturn); + + wss.sendRequestTableMessage(sharedLabRequest, new RequestCreatedMessage()).join(); + + verify(sApi, never()).getSessionsById(any()); + verify(aApi, times(1)).getAssignmentById(oopAssignment1.getId()); + + verify(wss, times(1)).sendMessage(peopleCaptor.capture(), eq("/topic/request-table"), + eq(new RequestCreatedMessage())); + assertThat(peopleCaptor.getValue()).containsExactlyInAnyOrderElementsOf(oopImportantPeople); + + } + +} diff --git a/src/test/java/test/TestDatabaseLoader.java b/src/test/java/test/TestDatabaseLoader.java index c0bb52896f038862c4fa552e43aa1554edfcfe46..52351105b968742a369bb6d621fa0dd4d83a3428 100644 --- a/src/test/java/test/TestDatabaseLoader.java +++ b/src/test/java/test/TestDatabaseLoader.java @@ -38,6 +38,7 @@ import nl.tudelft.queue.model.constraints.ClusterConstraint; import nl.tudelft.queue.model.constraints.ModuleDivisionConstraint; import nl.tudelft.queue.model.embeddables.*; import nl.tudelft.queue.model.enums.CommunicationMethod; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.enums.SelectionProcedure; import nl.tudelft.queue.model.events.*; @@ -266,9 +267,13 @@ public class TestDatabaseLoader { private RegularLab oopNowRegularLab2; private LabRequest[] oopNowRegularLab2Requests; + private LabRequest[] rlOopNowSharedLabRequests; + private RegularLab oopNowRegularLab3; private RegularLab oopNowRegularLab4; + private RegularLab oopNowRegularHybridLab1; + private RegularLab oop20RegLab; private RegularLab oop20RegLabDeleted; @@ -458,6 +463,22 @@ public class TestDatabaseLoader { .modules(Set.of(oopNowLabsModule.getId())) .requests(new ArrayList<>()) .build()); + + oopNowRegularHybridLab1 = lr.save(RegularLab.builder() + .session(createOopNowSessionForLab( + "Regular Hybrid Lab 1", + List.of(oopNowAssignments[0]), + List.of(roomPc1, roomCz1))) + .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) + .constraints(LabRequestConstraints.builder() + .build()) + .allowedRequests(Set.of( + AllowedRequest.of(oopNowAssignments[0].getId(), RequestType.QUESTION))) + .modules(Set.of(oopNowLabsModule.getId())) + .requests(new ArrayList<>()) + .onlineModes(Set.of(OnlineMode.JITSI)) + .build()); + } public <TS extends TimeSlot, T extends AbstractSlottedLab<TS>> T createSlotsForLab(T lab) { @@ -734,6 +755,8 @@ public class TestDatabaseLoader { oopNowRegularLab1Requests = new LabRequest[50]; oopNowRegularLab2Requests = new LabRequest[50]; + rlOopNowSharedLabRequests = new LabRequest[50]; + oopPastLectureRequests = new SelectionRequest[50]; for (int i = 50; i < 100; i++) { @@ -744,6 +767,9 @@ public class TestDatabaseLoader { RequestType.SUBMISSION, null); + var reqSharedLab1 = createLabRequest(rlOopNowSharedLab, students[i], + oopNowAssignments[i % oopNowAssignments.length], RequestType.SUBMISSION, null); + var reqPastLecture = createSelectionRequest(oopPastLecture, students[i]); createSelectionRequest(oopLectureRandom, students[i]); createSelectionRequest(oopLectureWeightLR, students[i]); @@ -769,6 +795,7 @@ public class TestDatabaseLoader { oopNowRegularLab1Requests[i - 50] = reqRegLab1; oopNowRegularLab2Requests[i - 50] = reqRegLab2; oopPastLectureRequests[i - 50] = reqPastLecture; + rlOopNowSharedLabRequests[i - 50] = reqSharedLab1; } } @@ -1295,8 +1322,9 @@ public class TestDatabaseLoader { var nAssignments = 3; var assignments = new AssignmentDetailsDTO[nAssignments]; for (int i = 0; i < nAssignments; i++) { + long assignmentId = module.getId() * 1000L + i; assignments[i] = aApiMocker.save(new AssignmentDetailsDTO() - .id(module.getId() * 1000L + i) + .id(assignmentId) .name("Assignment " + i) .sessions(new ArrayList<>()) .description(new Description().text("Nothing special, just an assignment")) @@ -1308,6 +1336,8 @@ public class TestDatabaseLoader { .submissions(new ArrayList<>()) .submissionLimit(null)); module.addAssignmentsItem(toView(assignments[i], AssignmentSummaryDTO.class)); + + aApiMocker.initializeAssignmentWithModule(assignmentId, module); } return assignments; } diff --git a/src/test/java/test/labracore/AssignmentApiMocker.java b/src/test/java/test/labracore/AssignmentApiMocker.java index c693c1e38b5ab8c996fb06cae56ca47b81afeb86..3bc15ad92d02904067bc54810142fab80366296f 100644 --- a/src/test/java/test/labracore/AssignmentApiMocker.java +++ b/src/test/java/test/labracore/AssignmentApiMocker.java @@ -20,25 +20,41 @@ package test.labracore; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import lombok.AllArgsConstructor; import nl.tudelft.labracore.api.AssignmentControllerApi; -import nl.tudelft.labracore.api.dto.AssignmentDetailsDTO; -import nl.tudelft.labracore.api.dto.AssignmentSummaryDTO; +import nl.tudelft.labracore.api.dto.*; import org.modelmapper.ModelMapper; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import com.google.common.base.Predicates; +import com.google.common.collect.Maps; @AllArgsConstructor public class AssignmentApiMocker extends LabracoreApiMocker<AssignmentDetailsDTO, Long> { private final AssignmentControllerApi aApi; + private final Map<Long, AssignmentModuleDetailsDTO> assignmentsWithModules = new HashMap<>(); + @Override public void mock() { when(aApi.getAllAssignmentsById(any())).thenAnswer(getAllByIds); + when(aApi.getAssignmentById(any())).thenReturn(Mono.empty()); + + when(aApi.getAssignmentsWithModules(any())).thenAnswer(invocation -> { + List<Long> ids = invocation.getArgument(0); + return Flux.fromIterable(Maps.filterKeys(assignmentsWithModules, Predicates.in(ids)).values()); + }); + when(aApi.getAllAssignments()).thenAnswer(invocation -> { var mapper = new ModelMapper(); var summaries = data.values().stream() @@ -49,8 +65,27 @@ public class AssignmentApiMocker extends LabracoreApiMocker<AssignmentDetailsDTO }); } + @Override + public AssignmentDetailsDTO save(AssignmentDetailsDTO dto) { + super.save(dto); + + var assignmentWithModule = mapper.map(dto, AssignmentModuleDetailsDTO.class); + + assignmentsWithModules.put(Objects.requireNonNull(dto.getId()), assignmentWithModule); + + return dto; + } + @Override public Long getId(AssignmentDetailsDTO dto) { return dto.getId(); } + + public void initializeAssignmentWithModule(long assignmentId, ModuleDetailsDTO module) { + var moduleSummary = Objects.requireNonNull(assignmentsWithModules.get(assignmentId)); + + var mappedModule = mapper.map(module, ModuleLayer1DTO.class); + + moduleSummary.setModule(mappedModule); + } }