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