From d30640ae3e2eb080fb5a23f43ca5ca985d31eedd Mon Sep 17 00:00:00 2001 From: Ruben Backx <r.w.backx@tudelft.nl> Date: Thu, 31 Aug 2023 14:54:37 +0200 Subject: [PATCH] Add a presentation mode for labs --- CHANGELOG.md | 1 + .../queue/controller/LabController.java | 85 ++++++- .../dto/create/CustomSlideCreateDTO.java | 46 ++++ .../queue/dto/patch/PresentationPatchDTO.java | 56 +++++ .../model/embeddables/DefaultSlides.java | 55 ++++ .../java/nl/tudelft/queue/model/labs/Lab.java | 11 +- .../tudelft/queue/model/misc/CustomSlide.java | 51 ++++ .../queue/model/misc/Presentation.java | 59 +++++ .../repository/CustomSlideRepository.java | 23 ++ .../repository/PresentationRepository.java | 23 ++ .../queue/security/DevSecurityConfig.java | 1 + .../security/ProductionSecurityConfig.java | 1 + .../nl/tudelft/queue/service/LabService.java | 16 ++ src/main/resources/migrations.yml | 119 +++++++++ src/main/resources/scss/presentation.scss | 88 +++++++ .../templates/lab/presentation/edit.html | 161 ++++++++++++ .../templates/lab/presentation/view.html | 237 ++++++++++++++++++ src/main/resources/templates/lab/view.html | 6 +- .../queue/controller/LabControllerTest.java | 28 +++ 19 files changed, 1057 insertions(+), 10 deletions(-) create mode 100644 src/main/java/nl/tudelft/queue/dto/create/CustomSlideCreateDTO.java create mode 100644 src/main/java/nl/tudelft/queue/dto/patch/PresentationPatchDTO.java create mode 100644 src/main/java/nl/tudelft/queue/model/embeddables/DefaultSlides.java create mode 100644 src/main/java/nl/tudelft/queue/model/misc/CustomSlide.java create mode 100644 src/main/java/nl/tudelft/queue/model/misc/Presentation.java create mode 100644 src/main/java/nl/tudelft/queue/repository/CustomSlideRepository.java create mode 100644 src/main/java/nl/tudelft/queue/repository/PresentationRepository.java create mode 100644 src/main/resources/scss/presentation.scss create mode 100644 src/main/resources/templates/lab/presentation/edit.html create mode 100644 src/main/resources/templates/lab/presentation/view.html diff --git a/CHANGELOG.md b/CHANGELOG.md index a54d5b584..ca6c77f7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Editions can now be archived by teachers. [@mmadara](https://gitlab.ewi.tudelft.nl/mmadara) - Labs can now be 'bilingual'. This allows students to indicate they wish to do their request only in Dutch or in Dutch or English. They will then get matched with a TA that speaks a matching language. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) - Students now get a notification when they are in front of the queue. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Labs now have a customisable presentation. This can be accessed by everyone with the present url to be presented on the screens. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) ### Changed - Course editions not created in Queue will now not be displayed until the teacher 'unhides' them. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) diff --git a/src/main/java/nl/tudelft/queue/controller/LabController.java b/src/main/java/nl/tudelft/queue/controller/LabController.java index a09c5181d..c3fcb5741 100644 --- a/src/main/java/nl/tudelft/queue/controller/LabController.java +++ b/src/main/java/nl/tudelft/queue/controller/LabController.java @@ -34,6 +34,7 @@ import javax.transaction.Transactional; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.librador.dto.create.Create; import nl.tudelft.librador.resolver.annotations.PathEntity; import nl.tudelft.queue.cache.*; import nl.tudelft.queue.csv.EmptyCsvException; @@ -44,6 +45,7 @@ import nl.tudelft.queue.dto.create.labs.RegularLabCreateDTO; import nl.tudelft.queue.dto.create.labs.SlottedLabCreateDTO; import nl.tudelft.queue.dto.create.requests.LabRequestCreateDTO; import nl.tudelft.queue.dto.create.requests.SelectionRequestCreateDTO; +import nl.tudelft.queue.dto.patch.PresentationPatchDTO; import nl.tudelft.queue.dto.patch.RequestPatchDTO; import nl.tudelft.queue.dto.patch.SlottedLabTimeSlotCapacityPatchDTO; import nl.tudelft.queue.dto.patch.labs.CapacitySessionPatchDTO; @@ -58,16 +60,17 @@ import nl.tudelft.queue.model.enums.QueueSessionType; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.enums.SelectionProcedure; import nl.tudelft.queue.model.labs.*; +import nl.tudelft.queue.model.misc.CustomSlide; +import nl.tudelft.queue.repository.CustomSlideRepository; +import nl.tudelft.queue.repository.PresentationRepository; import nl.tudelft.queue.repository.QueueSessionRepository; import nl.tudelft.queue.service.*; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; -import org.springframework.http.ContentDisposition; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.http.*; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; @@ -80,6 +83,12 @@ public class LabController { @Autowired private QueueSessionRepository qsRepository; + @Autowired + private PresentationRepository pr; + + @Autowired + private CustomSlideRepository csr; + @Autowired private LabService ls; @@ -822,4 +831,72 @@ public class LabController { return "error/403"; } + /** + * Gets the presentation page for a lab. + * + * @param session The lab + * @param room The room to present in + * @return The presentation page + */ + @GetMapping("/present/{session}") + public String getLabPresentation(@PathEntity QueueSession<?> session, + @RequestParam(required = false) Long room, Model model) { + if (!(session instanceof Lab lab)) + throw new ResourceNotFoundException(); + + model.addAttribute("lab", lab.toViewDTO()); + model.addAttribute("presentation", ls.getLabPresentation(lab)); + if (room != null) { + model.addAttribute("room", rCache.getRequired(room)); + } + + return "lab/presentation/view"; + } + + /** + * Gets the edit page for a lab presentation. + * + * @param session The session to edit the presentation for + * @return The edit page + */ + @GetMapping("/lab/{session}/presentation/edit") + @PreAuthorize("@permissionService.canManageSession(#session)") + public String getLabPresentationEditPage(@PathEntity QueueSession<?> session, Model model) { + if (!(session instanceof Lab lab)) + throw new ResourceNotFoundException(); + + ls.setOrganizationInModel(lab, model); + model.addAttribute("lab", lab.toViewDTO()); + model.addAttribute("presentation", ls.getLabPresentation(lab)); + + return "lab/presentation/edit"; + } + + /** + * Edits a lab presentation. + * + * @param session The session to edit + * @param patch The patch with the new presentation + * @return Redirect to the lab page + */ + @PostMapping("/lab/{session}/presentation/edit") + @PreAuthorize("@permissionService.canManageSession(#session)") + public String editLabPresentation(@PathEntity QueueSession<?> session, PresentationPatchDTO patch) { + if (!(session instanceof Lab lab)) + throw new ResourceNotFoundException(); + + csr.deleteAll(lab.getPresentation().getCustomSlides()); + lab.getPresentation().getCustomSlides().clear(); + if (patch.getCustomSlides() != null) { + List<CustomSlide> customSlides = patch.getCustomSlides().stream().map(Create::apply) + .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); + customSlides.forEach(s -> s.setPresentation(lab.getPresentation())); + lab.getPresentation().setCustomSlides(customSlides); + csr.saveAll(customSlides); + } + pr.save(patch.apply(lab.getPresentation())); + + return "redirect:/lab/{session}"; + } + } diff --git a/src/main/java/nl/tudelft/queue/dto/create/CustomSlideCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/CustomSlideCreateDTO.java new file mode 100644 index 000000000..260bedb22 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/dto/create/CustomSlideCreateDTO.java @@ -0,0 +1,46 @@ +/* + * 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.dto.create; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import nl.tudelft.librador.dto.create.Create; +import nl.tudelft.queue.model.misc.CustomSlide; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CustomSlideCreateDTO extends Create<CustomSlide> { + + @NotBlank + private String title; + + @NotNull + private String content; + + @Override + public Class<CustomSlide> clazz() { + return CustomSlide.class; + } +} diff --git a/src/main/java/nl/tudelft/queue/dto/patch/PresentationPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/PresentationPatchDTO.java new file mode 100644 index 000000000..40f18e4ba --- /dev/null +++ b/src/main/java/nl/tudelft/queue/dto/patch/PresentationPatchDTO.java @@ -0,0 +1,56 @@ +/* + * 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.dto.patch; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import nl.tudelft.librador.dto.patch.Patch; +import nl.tudelft.queue.dto.create.CustomSlideCreateDTO; +import nl.tudelft.queue.model.misc.Presentation; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PresentationPatchDTO extends Patch<Presentation> { + + private Boolean showCurrentRoom; + private Boolean showQueueSite; + private Boolean showLabInfo; + private Boolean showFeedbackReminder; + private Boolean showExamEnrolment; + + private List<CustomSlideCreateDTO> customSlides; + + @Override + protected void applyOneToOne() { + data.getDefaultSlides().setShowCurrentRoom(Boolean.TRUE.equals(showCurrentRoom)); + data.getDefaultSlides().setShowQueueSite(Boolean.TRUE.equals(showQueueSite)); + data.getDefaultSlides().setShowLabInfo(Boolean.TRUE.equals(showLabInfo)); + data.getDefaultSlides().setShowFeedbackReminder(Boolean.TRUE.equals(showFeedbackReminder)); + data.getDefaultSlides().setShowExamEnrolment(Boolean.TRUE.equals(showExamEnrolment)); + } + + @Override + protected void validate() { + } +} diff --git a/src/main/java/nl/tudelft/queue/model/embeddables/DefaultSlides.java b/src/main/java/nl/tudelft/queue/model/embeddables/DefaultSlides.java new file mode 100644 index 000000000..9b267d1ed --- /dev/null +++ b/src/main/java/nl/tudelft/queue/model/embeddables/DefaultSlides.java @@ -0,0 +1,55 @@ +/* + * 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.embeddables; + +import javax.persistence.Embeddable; +import javax.validation.constraints.NotNull; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +public class DefaultSlides { + + @NotNull + @Builder.Default + private Boolean showCurrentRoom = true; + + @NotNull + @Builder.Default + private Boolean showQueueSite = true; + + @NotNull + @Builder.Default + private Boolean showLabInfo = true; + + @NotNull + @Builder.Default + private Boolean showFeedbackReminder = true; + + @NotNull + @Builder.Default + private Boolean showExamEnrolment = true; + +} 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 e88fa1f7f..ab0c9e406 100644 --- a/src/main/java/nl/tudelft/queue/model/labs/Lab.java +++ b/src/main/java/nl/tudelft/queue/model/labs/Lab.java @@ -24,10 +24,7 @@ import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.persistence.ElementCollection; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; +import javax.persistence.*; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotEmpty; @@ -43,6 +40,7 @@ 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.misc.Presentation; import nl.tudelft.queue.service.LabService; import org.hibernate.validator.constraints.UniqueElements; @@ -98,6 +96,11 @@ public abstract class Lab extends QueueSession<LabRequest> { @Builder.Default private Boolean enableExperimental = false; + @OneToOne(mappedBy = "lab") + @ToString.Exclude + @EqualsAndHashCode.Exclude + private Presentation presentation; + /** * Sets the allowed request types for this lab using a map of assignments to the request types that are * allowed to be created for that assignment. diff --git a/src/main/java/nl/tudelft/queue/model/misc/CustomSlide.java b/src/main/java/nl/tudelft/queue/model/misc/CustomSlide.java new file mode 100644 index 000000000..f59ac7c0d --- /dev/null +++ b/src/main/java/nl/tudelft/queue/model/misc/CustomSlide.java @@ -0,0 +1,51 @@ +/* + * 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.misc; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CustomSlide { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + private String title; + + @Lob + @NotNull + private String content; + + @NotNull + @ManyToOne + private Presentation presentation; + +} diff --git a/src/main/java/nl/tudelft/queue/model/misc/Presentation.java b/src/main/java/nl/tudelft/queue/model/misc/Presentation.java new file mode 100644 index 000000000..e353965b4 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/model/misc/Presentation.java @@ -0,0 +1,59 @@ +/* + * 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.misc; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +import lombok.*; +import nl.tudelft.queue.model.embeddables.DefaultSlides; +import nl.tudelft.queue.model.labs.Lab; + +@Data +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Presentation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Embedded + @Builder.Default + private DefaultSlides defaultSlides = new DefaultSlides(); + + @NotNull + @Builder.Default + @ToString.Exclude + @EqualsAndHashCode.Exclude + @OneToMany(mappedBy = "presentation") + private List<CustomSlide> customSlides = new ArrayList<>(); + + @NotNull + @ToString.Exclude + @EqualsAndHashCode.Exclude + @OneToOne + private Lab lab; + +} diff --git a/src/main/java/nl/tudelft/queue/repository/CustomSlideRepository.java b/src/main/java/nl/tudelft/queue/repository/CustomSlideRepository.java new file mode 100644 index 000000000..2a0dba238 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/repository/CustomSlideRepository.java @@ -0,0 +1,23 @@ +/* + * 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.repository; + +import nl.tudelft.queue.model.misc.CustomSlide; + +public interface CustomSlideRepository extends QueueRepository<CustomSlide, Long> { +} diff --git a/src/main/java/nl/tudelft/queue/repository/PresentationRepository.java b/src/main/java/nl/tudelft/queue/repository/PresentationRepository.java new file mode 100644 index 000000000..0d4b7c3b8 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/repository/PresentationRepository.java @@ -0,0 +1,23 @@ +/* + * 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.repository; + +import nl.tudelft.queue.model.misc.Presentation; + +public interface PresentationRepository extends QueueRepository<Presentation, Long> { +} diff --git a/src/main/java/nl/tudelft/queue/security/DevSecurityConfig.java b/src/main/java/nl/tudelft/queue/security/DevSecurityConfig.java index 46111371a..1f38e0fd7 100644 --- a/src/main/java/nl/tudelft/queue/security/DevSecurityConfig.java +++ b/src/main/java/nl/tudelft/queue/security/DevSecurityConfig.java @@ -36,6 +36,7 @@ public class DevSecurityConfig extends LabradorSecurityConfigurerAdapter { .authorizeRequests() // Production permissions .antMatchers("/").permitAll() + .antMatchers("/present/**").permitAll() .antMatchers("/manifest.json").permitAll() .antMatchers("/favicon.ico").permitAll() .antMatchers("/sw.js").permitAll() diff --git a/src/main/java/nl/tudelft/queue/security/ProductionSecurityConfig.java b/src/main/java/nl/tudelft/queue/security/ProductionSecurityConfig.java index 5700c640c..7b3c06840 100644 --- a/src/main/java/nl/tudelft/queue/security/ProductionSecurityConfig.java +++ b/src/main/java/nl/tudelft/queue/security/ProductionSecurityConfig.java @@ -34,6 +34,7 @@ public class ProductionSecurityConfig extends LabradorSecurityConfigurerAdapter http .authorizeRequests() .antMatchers("/").permitAll() + .antMatchers("/present/**").permitAll() .antMatchers("/manifest.json").permitAll() .antMatchers("/favicon.ico").permitAll() .antMatchers("/sw.js").permitAll() diff --git a/src/main/java/nl/tudelft/queue/service/LabService.java b/src/main/java/nl/tudelft/queue/service/LabService.java index 111111920..3c65d2206 100644 --- a/src/main/java/nl/tudelft/queue/service/LabService.java +++ b/src/main/java/nl/tudelft/queue/service/LabService.java @@ -59,9 +59,11 @@ import nl.tudelft.queue.model.labs.CapacitySession; import nl.tudelft.queue.model.labs.ExamLab; import nl.tudelft.queue.model.labs.Lab; import nl.tudelft.queue.model.labs.RegularLab; +import nl.tudelft.queue.model.misc.Presentation; import nl.tudelft.queue.properties.QueueProperties; import nl.tudelft.queue.repository.LabRepository; import nl.tudelft.queue.repository.LabRequestConstraintRepository; +import nl.tudelft.queue.repository.PresentationRepository; import nl.tudelft.queue.repository.QueueSessionRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -112,6 +114,9 @@ public class LabService { @Autowired private QueueSessionRepository qsr; + @Autowired + private PresentationRepository pr; + @Autowired private CapacitySessionService css; @@ -548,4 +553,15 @@ public class LabService { exam.setPickedStudents(persons.stream().map(PersonDetailsDTO::getId).collect(Collectors.toSet())); } + @Transactional + public Presentation getLabPresentation(Lab lab) { + if (lab.getPresentation() == null) { + Presentation presentation = new Presentation(); + presentation.setLab(lab); + lab.setPresentation(presentation); + pr.save(presentation); + } + return lab.getPresentation(); + } + } diff --git a/src/main/resources/migrations.yml b/src/main/resources/migrations.yml index f9894658b..84ef1a716 100644 --- a/src/main/resources/migrations.yml +++ b/src/main/resources/migrations.yml @@ -1115,4 +1115,123 @@ databaseChangeLog: defaultValueBoolean: false tableName: lab + # Presentation mode + - changeSet: + id: 1693487015630-1 + author: ruben (generated) + changes: + - createTable: + columns: + - column: + autoIncrement: true + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_27 + name: id + type: BIGINT + - column: + constraints: + nullable: false + name: content + type: CLOB + - column: + name: title + type: VARCHAR(255) + - column: + constraints: + nullable: false + name: presentation_id + type: BIGINT + tableName: custom_slide + - changeSet: + id: 1693487015630-2 + author: ruben (generated) + changes: + - createTable: + columns: + - column: + autoIncrement: true + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_29 + name: id + type: BIGINT + - column: + name: show_current_room + type: BOOLEAN + defaultValueBoolean: true + - column: + name: show_exam_enrolment + type: BOOLEAN + defaultValueBoolean: true + - column: + name: show_feedback_reminder + type: BOOLEAN + defaultValueBoolean: true + - column: + name: show_lab_info + type: BOOLEAN + defaultValueBoolean: true + - column: + name: show_queue_site + type: BOOLEAN + defaultValueBoolean: true + - column: + constraints: + nullable: false + name: lab_id + type: BIGINT + tableName: presentation + - changeSet: + id: 1693487015630-3 + author: ruben (generated) + changes: + - createIndex: + columns: + - column: + name: presentation_id + indexName: FK1a2hhl7sxkiifm7pyuiks20ct_INDEX_2 + tableName: custom_slide + - changeSet: + id: 1693487015630-4 + author: ruben (generated) + changes: + - createIndex: + columns: + - column: + name: lab_id + indexName: FKej2rd9k3t4x1r4tio0grj8br_INDEX_2 + tableName: presentation + - changeSet: + id: 1693487015630-5 + author: ruben (generated) + changes: + - addForeignKeyConstraint: + baseColumnNames: presentation_id + baseTableName: custom_slide + constraintName: FK1a2hhl7sxkiifm7pyuiks20ct + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: presentation + validate: true + - changeSet: + id: 1693487015630-6 + author: ruben (generated) + changes: + - addForeignKeyConstraint: + baseColumnNames: lab_id + baseTableName: presentation + constraintName: FKej2rd9k3t4x1r4tio0grj8br + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: lab + validate: true diff --git a/src/main/resources/scss/presentation.scss b/src/main/resources/scss/presentation.scss new file mode 100644 index 000000000..263b08d2b --- /dev/null +++ b/src/main/resources/scss/presentation.scss @@ -0,0 +1,88 @@ +.presentation { + --primary-colour: #00a6d6; +} + +.slide { + &:not([data-current]) { + display: none; + } + + background-color: white; + display: flex; + flex-direction: column; + height: 100cqh; + position: relative; + padding-left: calc(24cqw); + padding-top: 3cqh; + + &__sidebar { + align-items: end; + background-color: var(--primary-colour); + display: flex; + left: 0; + inset-block: 0; + padding: 2cqw; + position: absolute; + width: 20cqw; + + img { + width: 50%; + } + } + + &__title { + color: var(--primary-colour); + font-size: 6.25cqw; + text-align: center; + margin-bottom: 2.08cqw; + } + + &__content { + display: grid; + justify-items: center; + flex-grow: 1; + font-size: 2cqw; + + p + * { + margin-top: 1.65cqw; + } + + ul, + ol { + margin-left: 2.9cqw; + margin-bottom: 0.85cqw; + } + + em { + font-style: italic; + } + strong { + font-weight: 500; + } + + h1 { + font-size: 5.4cqw; + } + h2 { + font-size: 4.15cqw; + } + h3 { + font-size: 2.9cqw; + } + h4 { + font-size: 2.5cqw; + } + h5 { + font-size: 2.3cqw; + } + } + + &__footer { + display: flex; + font-size: 1.85cqw; + font-weight: 500; + justify-content: space-between; + min-height: 4cqw; + padding-right: 4cqw; + } +} diff --git a/src/main/resources/templates/lab/presentation/edit.html b/src/main/resources/templates/lab/presentation/edit.html new file mode 100644 index 000000000..cb70b3040 --- /dev/null +++ b/src/main/resources/templates/lab/presentation/edit.html @@ -0,0 +1,161 @@ +<!-- + + Queue - A Queueing system that can be used to handle labs in higher education + Copyright (C) 2016-2020 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="~{edition/view}"> + <head> + <link rel="stylesheet" href="/css/presentation.css" /> + + <link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css" /> + <script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script> + </head> + + <body> + <form class="flex vertical" layout:fragment="subcontent" id="form" th:action="@{/lab/{id}/presentation/edit(id=${lab.id})}" method="post"> + <h3 class="font-500">Edit presentation</h3> + + <div> + <div> + <h4 class="font-400 mb-2">Default slides</h4> + <div> + <div> + <input + id="show-current-room" + name="showCurrentRoom" + type="checkbox" + th:checked="${presentation.defaultSlides.showCurrentRoom}" /> + <label for="show-current-room">Include current room slide</label> + </div> + <div> + <input + id="show-queue-site" + name="showQueueSite" + type="checkbox" + th:checked="${presentation.defaultSlides.showQueueSite}" /> + <label for="show-queue-site">Include Queue site slide</label> + </div> + <div> + <input id="show-lab-info" name="showLabInfo" type="checkbox" th:checked="${presentation.defaultSlides.showLabInfo}" /> + <label for="show-lab-info">Include lab info slide</label> + </div> + <div> + <input + id="show-feedback-reminder" + name="showFeedbackReminder" + type="checkbox" + th:checked="${presentation.defaultSlides.showFeedbackReminder}" /> + <label for="show-feedback-reminder">Include TA feedback reminder slide</label> + </div> + <div> + <input + id="show-exam-enrolment" + name="showExamEnrolment" + type="checkbox" + th:checked="${presentation.defaultSlides.showExamEnrolment}" /> + <label for="show-exam-enrolment">Include exam enrolment slide</label> + </div> + </div> + </div> + </div> + + <div class="underlined"></div> + + <div> + <h4 class="font-400 mb-3">Custom slides</h4> + <div class="flex align-center"> + <label for="custom-slide-amount">Amount</label> + <input id="custom-slide-amount" class="textfield" type="number" min="0" th:value="${presentation.customSlides.size()}" /> + </div> + <div id="custom-slides" class="flex vertical mt-5" th:classappend="${presentation.customSlides.size() == 0} ? 'hidden' : ''"> + <div th:each="slide, iter : ${presentation.customSlides}"> + <input class="textfield mb-3" th:name="|customSlides[${iter.index}].title|" th:value="${slide.title}" /> + <textarea + th:id="|custom-slide-${iter.index}|" + th:name="|customSlides[${iter.index}].content|" + th:text="${slide.content}"></textarea> + </div> + </div> + </div> + + <div class="underlined"></div> + + <div> + <button class="button" type="submit">Save</button> + </div> + + <script th:inline="javascript" type="text/javascript"> + let ides = []; + + document.getElementById("form").addEventListener("submit", function () { + for (let i = 0; i < ides.length; i++) { + document.getElementById(`custom-slide-${i}`).innerText = ides[i].value(); + } + }); + + const customSlides = document.getElementById("custom-slides"); + + const customContent = customSlides.querySelectorAll("textarea"); + for (let i = 0; i < customContent.length; i++) { + ides.push( + new SimpleMDE({ + element: customContent[i], + forceSync: true, + }) + ); + } + + document.getElementById("custom-slide-amount").addEventListener("change", function () { + const oldAmount = customSlides.children.length; + + if (this.value === "0") { + customSlides.classList.add("hidden"); + } + + if (oldAmount > this.value) { + customSlides.children[customSlides.children.length - 1].remove(); + ides.pop(); + } else { + customSlides.classList.remove("hidden"); + + const div = document.createElement("div"); + const label = document.createElement("input"); + label.classList.add("textfield", "mb-3"); + label.setAttribute("name", `customSlides[${this.value - 1}].title`); + label.setAttribute("placeholder", "Slide title"); + label.value = `Slide ${this.value}`; + div.appendChild(label); + const slide = document.createElement("textarea"); + slide.setAttribute("id", `custom-slide-${this.value}`); + slide.setAttribute("name", `customSlides[${this.value - 1}].content`); + slide.innerText = "Slide content"; + div.appendChild(slide); + customSlides.appendChild(div); + + ides.push( + new SimpleMDE({ + element: slide, + forceSync: true, + }) + ); + } + }); + </script> + </form> + </body> +</html> diff --git a/src/main/resources/templates/lab/presentation/view.html b/src/main/resources/templates/lab/presentation/view.html new file mode 100644 index 000000000..73ca4b2f5 --- /dev/null +++ b/src/main/resources/templates/lab/presentation/view.html @@ -0,0 +1,237 @@ +<!-- + + Queue - A Queueing system that can be used to handle labs in higher education + Copyright (C) 2016-2020 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="~{layout}"> + <head> + <link rel="stylesheet" href="/css/presentation.css" /> + <script src="/webjars/momentjs/min/moment.min.js"></script> + </head> + + <body> + <main layout:fragment="container" style="height: 100%; container-type: size"> + <form class="flex vertical align-center" method="get" th:if="${room == null}"> + <h2 class="font-800 mbl-5">Select room</h2> + <div class="grid auto-fit" style="min-width: var(--content-width)"> + <button + name="room" + th:value="${room.id}" + th:each="room : ${lab.session.rooms}" + th:text="|${room.building.name} - ${room.name}|" + class="button"></button> + </div> + </form> + + <div class="presentation" th:if="${room != null}" layout:fragment="presentation"> + <div class="slide" th:if="${presentation.defaultSlides.showCurrentRoom}"> + <div class="slide__sidebar"> + <img th:src="@{/img/tudelft_logo_light.png}" alt="TU Delft" /> + </div> + + <h2 class="slide__title">You are in</h2> + + <div class="slide__content"> + <div> + <h1 + style="color: var(--primary-colour); text-align: center; font-size: 7cqw; font-weight: 500" + th:text="|${room.building.name} - ${room.name}|"></h1> + + <div class="flex justify-center"> + <div id="image-holder" hidden> + <img style="height: 24cqw" alt="Room map" /> + </div> + </div> + </div> + </div> + + <div class="slide__footer"> + <span style="color: var(--primary-colour)">https://queue.tudelft.nl/</span> + <span></span> + </div> + </div> + + <div class="slide" th:if="${presentation.defaultSlides.showQueueSite}"> + <div class="slide__sidebar"> + <img th:src="@{/img/tudelft_logo_light.png}" alt="tu delft" /> + </div> + + <h2 class="slide__title">Do you have a question?</h2> + + <div class="slide__content"> + <p style="font-size: 3cqw"> + Enqueue at + <span class="fw-500" style="color: var(--primary-colour)">https://queue.tudelft.nl/</span> + . + </p> + </div> + + <div class="slide__footer"> + <span></span> + <span th:text="|This is ${room.building.name} - ${room.name}|"></span> + </div> + </div> + + <div class="slide" th:if="${presentation.defaultSlides.showLabInfo}"> + <div class="slide__sidebar"> + <img th:src="@{/img/tudelft_logo_light.png}" alt="tu delft" /> + </div> + + <h2 class="slide__title">Session Information</h2> + + <div class="slide__content"> + <div th:utext="${lab.extraInfo}" style="width: 100%"></div> + </div> + + <div class="slide__footer"> + <span style="color: var(--primary-colour)">https://queue.tudelft.nl/</span> + <span th:text="|This is ${room.building.name} - ${room.name}|"></span> + </div> + </div> + + <div class="slide" th:if="${presentation.defaultSlides.showFeedbackReminder}"> + <div class="slide__sidebar"> + <img th:src="@{/img/tudelft_logo_light.png}" alt="tu delft" /> + </div> + + <h2 class="slide__title">TA Feedback</h2> + + <div class="slide__content"> + <div class="flex vertical p-0 align-center" style="font-size: 3cqw"> + <p style="text-align: center"> + Did the TA help you well? + <br /> + Can they do something to explain it better? + </p> + <p>Leave feedback. We appreciate it!</p> + <p>Your feedback is fully anonymous.</p> + </div> + </div> + + <div class="slide__footer"> + <span style="color: var(--primary-colour)">https://queue.tudelft.nl/</span> + <span th:text="|This is ${room.building.name} - ${room.name}|"></span> + </div> + </div> + + <div class="slide" th:if="${presentation.defaultSlides.showExamEnrolment}"> + <div class="slide__sidebar"> + <img th:src="@{/img/tudelft_logo_light.png}" alt="tu delft" /> + </div> + + <h2 class="slide__title">Enrol for your exams!</h2> + + <div class="slide__content"> + <div class="flex vertical align-center gap-0"> + <h1>My TU Delft</h1> + <h2 style="color: var(--primary-colour)">https://my.tudelft.nl/</h2> + </div> + </div> + + <div class="slide__footer"> + <span style="color: var(--primary-colour)">https://queue.tudelft.nl/</span> + <span th:text="|This is ${room.building.name} - ${room.name}|"></span> + </div> + </div> + + <div class="slide" th:each="slide : ${presentation.customSlides}"> + <div class="slide__sidebar"> + <img th:src="@{/img/tudelft_logo_light.png}" alt="tu delft" /> + </div> + <h2 class="slide__title" th:text="${slide.title}"></h2> + <div class="slide__content"> + <div th:utext="${slide.content}" style="width: 100%"></div> + </div> + <div class="slide__footer"> + <span style="color: var(--primary-colour)">https://queue.tudelft.nl/</span> + <span th:text="|This is ${room.building.name} - ${room.name}|"></span> + </div> + </div> + + <div class="slide" id="session-over-slide"> + <div class="slide__sidebar"> + <img th:src="@{/img/tudelft_logo_light.png}" alt="tu delft" /> + </div> + <h2 class="slide__title">This session is now over</h2> + <div class="slide__content"></div> + <div class="slide__footer"></div> + </div> + + <script src="/js/map_loader.js"></script> + <script type="text/javascript" th:inline="javascript"> + const roomId = /*[[${room.id}]]*/ 0; + updateRequestInfo(roomId); + </script> + <script type="text/javascript" th:inline="javascript"> + let sessionEnd = /*[[${lab.session.endTime}]]*/ null; + </script> + + <script> + function prevSlide() { + const current = document.querySelector("[data-current]"); + let prev = current.previousElementSibling; + if (prev == null || !prev.classList.contains("slide")) { + prev = document.querySelector(".slide:last-of-type"); + if (prev.id === "session-over-slide") { + prev = prev.previousElementSibling; + } + } + current.removeAttribute("data-current"); + prev.setAttribute("data-current", ""); + } + function nextSlide() { + const current = document.querySelector("[data-current]"); + let next = current.nextElementSibling; + if (next == null || !next.classList.contains("slide") || next.id === "session-over-slide") { + next = document.querySelector(".slide:first-of-type"); + } + current.removeAttribute("data-current"); + next.setAttribute("data-current", ""); + } + + document.addEventListener("DOMContentLoaded", function () { + const interval = setInterval(nextSlide, 10000); + + const keyHandler = function (event) { + switch (event.key) { + case "ArrowLeft": + prevSlide(); + break; + case "ArrowRight": + nextSlide(); + break; + } + }; + + setInterval(function () { + if (moment(sessionEnd).isBefore(moment())) { + clearInterval(interval); + document.querySelector("[data-current]").removeAttribute("data-current"); + document.getElementById("session-over-slide").setAttribute("data-current", ""); + document.removeEventListener("keydown", keyHandler); + } + }, 1000); + + document.querySelector(".slide:first-of-type").setAttribute("data-current", ""); + document.addEventListener("keydown", keyHandler); + }); + </script> + </div> + </main> + </body> +</html> diff --git a/src/main/resources/templates/lab/view.html b/src/main/resources/templates/lab/view.html index f86db548b..bf3bbac81 100644 --- a/src/main/resources/templates/lab/view.html +++ b/src/main/resources/templates/lab/view.html @@ -51,10 +51,10 @@ <body> <section layout:fragment="subcontent"> - <div class="flex space-between align-center mb-5"> + <div class="flex space-between align-center mb-5 | md:vertical md:align-start"> <h3 class="font-600" th:text="'Session #' + ${qSession.id} + ': ' + ${qSession.session.name}"></h3> - <div class="flex gap-3"> + <div class="flex gap-3 wrap"> <th:block th:if="${current == null && @permissionService.canEnqueueSelf(qSession.id)}"> <a th:href="@{/lab/{id}/enqueue(id=${qSession.id})}" @@ -66,6 +66,7 @@ </th:block> <th:block th:if="${@permissionService.canManageSession(qSession.data)}"> + <a class="button" data-style="outlined" target="_blank" th:href="@{/present/{id}(id=${qSession.id})}">Present</a> <a class="button" data-style="outlined" th:href="@{/lab/{id}/export(id=${qSession.id})}" download>Export lab</a> <form th:action="@{/lab/{id}/close-enqueue/{bool}(id=${qSession.id}, bool=${!qSession.enqueueClosed})}" method="post"> <button th:unless="${qSession.enqueueClosed}" class="button" data-style="outlined" data-type="error" type="submit"> @@ -76,6 +77,7 @@ </button> </form> + <a class="button" data-style="outlined" th:href="@{/lab/{id}/presentation/edit(id=${qSession.id})}">Edit presentation</a> <a th:href="@{/lab/{id}/edit(id=${qSession.id})}" class="button" data-style="outlined">Edit lab</a> </th:block> </div> diff --git a/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java b/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java index 91bc66f6c..43923182c 100644 --- a/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java +++ b/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java @@ -114,6 +114,9 @@ class LabControllerTest { @Autowired private SessionControllerApi sApi; + @Autowired + private LabRepository lr; + @Captor private ArgumentCaptor<QueueSession<LabRequest>> queueSessionArgumentCaptor; @@ -717,6 +720,31 @@ class LabControllerTest { .andExpect(view().name("error/403")); } + @Test + void getPresentationIsAccessibleForEveryone() throws Exception { + mvc.perform(get("/present/{id}", regLab1.getId())) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("admin") + void getEditPresentation() throws Exception { + mvc.perform(get("/lab/{id}/presentation/edit", regLab1.getId())) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails("admin") + void editPresentation() throws Exception { + mvc.perform(get("/lab/{id}/presentation/edit", regLab1.getId())) + .andExpect(status().isOk()); + mvc.perform( + post("/lab/{id}/presentation/edit?showFeedbackReminder=false", regLab1.getId()).with(csrf())) + .andExpect(status().is3xxRedirection()); + assertThat(lr.getById(regLab1.getId()).getPresentation().getDefaultSlides().getShowFeedbackReminder()) + .isFalse(); + } + @ParameterizedTest @MethodSource(value = "protectedEndpoints") void requestWithoutUserDetailsGoesToLogin(MockHttpServletRequestBuilder request) throws Exception { -- GitLab