diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a837d317ce16a4e5835f5963e0dfb85234ceca..3d32a5fffda8341ad4b38401133016a7dbe3340e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ 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) ### Changed - Redirect students to their request when their are being processed when accessing the lab page. [@cedricwilleken](https://gitlab.ewi.tudelft.nl/cedricwilleken) 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/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..5474720be909e72747cbbcccd11120c1ea69f93a 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 @@ -34,6 +34,7 @@ import nl.tudelft.queue.dto.view.RequestViewDTO; 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 +51,7 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> { private String jitsiRoom; private TimeSlot timeSlot; + private OnlineMode onlineMode; private RequestType requestType; @@ -61,6 +63,10 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> { public void postApply() { super.postApply(); + if (onlineMode != null) { + setRoom(null); + } + assignment = getBean(AssignmentCacheManager.class).getOrThrow(data.getAssignment()); timeSlot = data.getTimeSlot(); @@ -93,6 +99,7 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> { "Comment", "Question", "Time Slot", + "Online Mode", "Request Type", "Assignment"); } @@ -103,6 +110,7 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> { (comment != null) ? comment : "", (question != null) ? question : "", (timeSlot != null) ? timeSlot.toString() : "", + (onlineMode != null) ? onlineMode.getDisplayName() : "", requestType.displayName(), assignment.getName()); } diff --git a/src/main/java/nl/tudelft/queue/model/LabRequest.java b/src/main/java/nl/tudelft/queue/model/LabRequest.java index d520298e9d53e5cbca493c465305c34306dfea9f..989cb6dd657131c5aba693b7d199553a4bf807c9 100644 --- a/src/main/java/nl/tudelft/queue/model/LabRequest.java +++ b/src/main/java/nl/tudelft/queue/model/LabRequest.java @@ -33,6 +33,7 @@ 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; @@ -72,6 +73,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. 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/LabRequestRepository.java b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java index 658c90981b4ea0f34eae60521f6ce2acf0853746..f38fe483b59cb2f45718a066dec3546e9e6c12f7 100644 --- a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java +++ b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java @@ -364,6 +364,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..6fafcbffb695972577644265b2534db8f695862e 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; @@ -486,4 +484,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/RequestService.java b/src/main/java/nl/tudelft/queue/service/RequestService.java index f73c23c6f6dffad50ec882b6bfb59c41d185720e..baf0ee3c416dec2a59f0a728b0049da86bfa9753 100644 --- a/src/main/java/nl/tudelft/queue/service/RequestService.java +++ b/src/main/java/nl/tudelft/queue/service/RequestService.java @@ -36,6 +36,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; @@ -123,7 +124,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()) { diff --git a/src/main/resources/migrations.yml b/src/main/resources/migrations.yml index b229e6f9f62cf2879a04c90d2422a08543c62eb3..f3844f29b6b05f670a3eabadac3717a92d687adb 100644 --- a/src/main/resources/migrations.yml +++ b/src/main/resources/migrations.yml @@ -795,3 +795,63 @@ 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: 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'; + + 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/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/test/java/nl/tudelft/queue/controller/LabControllerTest.java b/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java index c487fc419ec87a5eb069490570523165f54c52cf..cb5c0ded1240502fb92d36802ef78abf72d94ef8 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 { @@ -608,6 +651,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 +800,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/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/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/test/TestDatabaseLoader.java b/src/test/java/test/TestDatabaseLoader.java index c0bb52896f038862c4fa552e43aa1554edfcfe46..c9e53734b81642c2b3408a98f4cc51ac84cbb867 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.*; @@ -269,6 +270,8 @@ public class TestDatabaseLoader { private RegularLab oopNowRegularLab3; private RegularLab oopNowRegularLab4; + private RegularLab oopNowRegularHybridLab1; + private RegularLab oop20RegLab; private RegularLab oop20RegLabDeleted; @@ -458,6 +461,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) {