diff --git a/.gitignore b/.gitignore
index 98f8562eb5ff5a65301e48c1768d807d8fc2631d..46e64572953cd71e5971d533ee1b3b6cbc72735f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,3 +47,5 @@ src/main/resources/static/css/*.css
 *.css.map
 *.sass.map
 *.scss.map
+
+storage
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65a4c6e787b0bbb389b520e781217954739db34d..e8ac522566f30ca64014c06b24d13ec23ced19df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,18 +11,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Added
+
+### Changed
+
+### Fixed
+
+## [2.2.0]
+
 ### Added
  - 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)
+ - Students now get a reminder to give feedback to their TA after a request (this can be turned off). [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx)
+ - Requests on the lab page can now be filtered. [@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)
  - Shared editions are now displayed properly as upcoming/finished/archived. [@mmadara](https://gitlab.ewi.tudelft.nl/mmadara)
+ - If a module already has groups, students now need to join a group before enqueueing. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx)
+
 ### Fixed
  - Session names in the edition export did not correspond to the correct sessions. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) 
  - Completed lab info pages now load correctly. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
-
  - Session names in the edition export did not correspond to the correct sessions. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx)
  - File names from parameters are now filtered. [@mmadara](https://gitlab.ewi.tudelft.nl/mmadara)
+ - Course catalog can now be filtered again. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx)
+
+ - Revoked requests could be finished (approved, rejected, etc.) by teachers. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx)
+ - Room maps did not work. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx)
 ### Deprecated
 
 ### Removed
diff --git a/build.gradle.kts b/build.gradle.kts
index 792aa26ad2ef90b1a857bad11b2b9c2f08a4eb9b..39121623db56c46ebe974df974a7b013cf82a726 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -6,13 +6,13 @@ import org.springframework.boot.gradle.tasks.run.BootRun
 import java.nio.file.Files
 
 group = "nl.tudelft.ewi.queue"
-version = "2.1.4"
+version = "2.2.0"
 
 val javaVersion = JavaVersion.VERSION_17
 
 val libradorVersion = "1.2.2-SNAPSHOT"
 val labradoorVersion = "1.4.1-SNAPSHOT"
-val chihuahUIVersion = "1.0.1"
+val chihuahUIVersion = "1.0.2"
 val queryDslVersion = "4.4.0"
 
 // A definition of all dependencies and repositories where to find them that need to
diff --git a/src/main/java/nl/tudelft/queue/config/ThymeleafConfig.java b/src/main/java/nl/tudelft/queue/config/ThymeleafConfig.java
index a70352922926eee88b11bada31b3e136171f097b..bc9669dc49ec60bdf1b711200920be27c4a841e1 100644
--- a/src/main/java/nl/tudelft/queue/config/ThymeleafConfig.java
+++ b/src/main/java/nl/tudelft/queue/config/ThymeleafConfig.java
@@ -21,6 +21,8 @@ import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 
 import nl.tudelft.queue.dialect.AuthenticatedPersonDialect;
+import nl.tudelft.queue.dialect.ProfileDialect;
+import nl.tudelft.queue.repository.ProfileRepository;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -44,6 +46,11 @@ public class ThymeleafConfig {
 		return new AuthenticatedPersonDialect();
 	}
 
+	@Bean
+	public ProfileDialect profileDialect(ProfileRepository profileRepository) {
+		return new ProfileDialect(profileRepository);
+	}
+
 	/**
 	 * Checks whether today is the day.
 	 *
diff --git a/src/main/java/nl/tudelft/queue/controller/AssignmentController.java b/src/main/java/nl/tudelft/queue/controller/AssignmentController.java
index 7f1b31112707b53239ee7d09581cedd59e6b9d12..2f96b7d26548aabc0c34615ab46a7664bd2344b2 100644
--- a/src/main/java/nl/tudelft/queue/controller/AssignmentController.java
+++ b/src/main/java/nl/tudelft/queue/controller/AssignmentController.java
@@ -18,10 +18,8 @@
 package nl.tudelft.queue.controller;
 
 import nl.tudelft.labracore.api.AssignmentControllerApi;
-import nl.tudelft.labracore.api.dto.AssignmentCreateDTO;
-import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
-import nl.tudelft.labracore.api.dto.ModuleDetailsDTO;
-import nl.tudelft.labracore.api.dto.ModuleSummaryDTO;
+import nl.tudelft.labracore.api.StudentGroupControllerApi;
+import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.queue.cache.AssignmentCacheManager;
 import nl.tudelft.queue.cache.EditionCacheManager;
 import nl.tudelft.queue.cache.ModuleCacheManager;
@@ -50,6 +48,9 @@ public class AssignmentController {
 	@Autowired
 	private AssignmentControllerApi aApi;
 
+	@Autowired
+	private StudentGroupControllerApi sgApi;
+
 	/**
 	 * Gets the page for creating an assignment. This is a basic page with only the most basic of properties
 	 * for an assignment. Further adjustments or creation options should be made available in Portal or later
@@ -141,6 +142,24 @@ public class AssignmentController {
 		return "redirect:/edition/" + module.getEdition().getId() + "/modules";
 	}
 
+	/**
+	 * Get the page with a list of groups to join for an assignment.
+	 *
+	 * @param  assignmentId The id of the assignment
+	 * @return              The group page
+	 */
+	@GetMapping("/assignment/{assignmentId}/groups")
+	public String getAssignmentGroups(@PathVariable Long assignmentId, Model model) {
+		AssignmentDetailsDTO assignment = aCache.getRequired(assignmentId);
+		ModuleDetailsDTO module = mCache.getRequired(assignment.getModule().getId());
+
+		model.addAttribute("module", module);
+		model.addAttribute("edition", eCache.getRequired(module.getEdition().getId()));
+		model.addAttribute("groups", sgApi.getAllGroupsInModule(module.getId()).collectList().block());
+
+		return "module/groups";
+	}
+
 	/**
 	 * Adds attributes for the create assignment page to the given model.
 	 *
diff --git a/src/main/java/nl/tudelft/queue/controller/EditionController.java b/src/main/java/nl/tudelft/queue/controller/EditionController.java
index bf9bc3b20d3697d7cbe14f8801c8c0613e201e87..dbb007acefb922fd5fc7cd71db50dc95870027e7 100644
--- a/src/main/java/nl/tudelft/queue/controller/EditionController.java
+++ b/src/main/java/nl/tudelft/queue/controller/EditionController.java
@@ -171,7 +171,8 @@ public class EditionController {
 		eCache.register(lcEditions);
 		erCache.getAndIgnoreMissing(lcEditions.stream().map(EditionDetailsDTO::getId));
 
-		var editions = es.queueEditionDTO(lcEditions, QueueEditionDetailsDTO.class);
+		var editions = es.queueEditionDTO(es.filterEditions(lcEditions, filter),
+				QueueEditionDetailsDTO.class);
 		if (person.getDefaultRole() != DefaultRole.ADMIN) {
 			editions = editions.stream().filter(e -> !e.getHidden()).toList();
 		}
diff --git a/src/main/java/nl/tudelft/queue/controller/LabController.java b/src/main/java/nl/tudelft/queue/controller/LabController.java
index a09c5181d4e734f0e4ed0f1b4da2e6ae6d44b399..6d06f33f6e6d67b0d5c5fb0d9e45b1193d88ac23 100644
--- a/src/main/java/nl/tudelft/queue/controller/LabController.java
+++ b/src/main/java/nl/tudelft/queue/controller/LabController.java
@@ -17,6 +17,7 @@
  */
 package nl.tudelft.queue.controller;
 
+import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*;
 import static nl.tudelft.queue.service.LabService.SessionType.REGULAR;
 import static nl.tudelft.queue.service.LabService.SessionType.SHARED;
 
@@ -31,9 +32,11 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 import javax.transaction.Transactional;
 
+import nl.tudelft.labracore.api.StudentGroupControllerApi;
 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,30 +47,35 @@ 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;
 import nl.tudelft.queue.dto.patch.labs.ExamLabPatchDTO;
 import nl.tudelft.queue.dto.patch.labs.RegularLabPatchDTO;
 import nl.tudelft.queue.dto.patch.labs.SlottedLabPatchDTO;
+import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
 import nl.tudelft.queue.dto.view.RequestViewDTO;
 import nl.tudelft.queue.model.QueueSession;
 import nl.tudelft.queue.model.Request;
 import nl.tudelft.queue.model.embeddables.AllowedRequest;
+import nl.tudelft.queue.model.enums.Language;
 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.LabRequestRepository;
+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;
@@ -75,11 +83,22 @@ import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
+import com.google.common.collect.Maps;
+
 @Controller
 public class LabController {
 	@Autowired
 	private QueueSessionRepository qsRepository;
 
+	@Autowired
+	private PresentationRepository pr;
+
+	@Autowired
+	private CustomSlideRepository csr;
+
+	@Autowired
+	private LabRequestRepository lrr;
+
 	@Autowired
 	private LabService ls;
 
@@ -107,18 +126,27 @@ public class LabController {
 	@Autowired
 	private EditionCollectionCacheManager ecCache;
 
+	@Autowired
+	private EditionRolesCacheManager erCache;
+
 	@Autowired
 	private ModuleCacheManager mCache;
 
 	@Autowired
 	private SessionCacheManager sCache;
 
+	@Autowired
+	private StudentGroupCacheManager sgCache;
+
 	@Autowired
 	private RoomCacheManager rCache;
 
 	@Autowired
 	private RoleDTOService roleService;
 
+	@Autowired
+	private StudentGroupControllerApi sgApi;
+
 	@Autowired
 	private HttpSession session;
 
@@ -141,11 +169,22 @@ public class LabController {
 			Model model) {
 		setEnqueuePageAttributes(qSession, model, person);
 
+		RequestTableFilterDTO filter = rts.checkAndStoreFilterDTO(null, "/lab/" + qSession.getId());
+		model.addAttribute("filter", filter);
+
 		// Either get all requests for the lab if the person is an assistant or just those specific to the person.
-		//noinspection Convert2MethodRef
+		List<? extends Request<?>> requests;
+		if (qSession instanceof Lab lab) {
+			requests = lrr.findAllByFilter(List.of(lab), filter, Language.ANY);
+		} else {
+			requests = qSession.getRequests();
+		}
+		if (!ps.canViewSessionRequests(qSession.getId())) {
+			requests = requests.stream().filter(r -> Objects.equals(r.getRequester(), person.getId()))
+					.toList();
+		}
 		model.addAttribute("requests",
-				rts.convertRequestsToView(ps.canViewSessionRequests(qSession.getId()) ? qSession.getRequests()
-						: qSession.getAllRequestsForPerson(person.getId()))
+				rts.convertRequestsToView(requests)
 						.stream()
 						.sorted(Comparator.comparing((RequestViewDTO<?> r) -> r.getCreatedAt()).reversed())
 						.collect(Collectors.toList()));
@@ -168,7 +207,26 @@ public class LabController {
 					.ifPresent(r -> model.addAttribute("selectionResult", r.toViewDTO()));
 		}
 
-		model.addAttribute("modules", mCache.getAndIgnoreMissing(qSession.getModules().stream()));
+		List<ModuleDetailsDTO> modules = mCache.getAndIgnoreMissing(qSession.getModules().stream());
+		model.addAttribute("modules", modules);
+		// Filter data
+		model.addAttribute("allAssignments",
+				modules.stream().flatMap(m -> m.getAssignments().stream()).toList());
+		model.addAttribute("assignmentsWithCourseCodes",
+				modules.stream()
+						.flatMap(m -> m.getAssignments().stream()
+								.map(a -> Maps.immutableEntry(a.getId(),
+										eCache.getRequired(m.getEdition().getId()).getCourse().getCode())))
+						.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
+		model.addAttribute("assistants", erCache
+				.getAndIgnoreMissing(
+						qSession.getSessionDto().getEditions().stream().map(EditionSummaryDTO::getId))
+				.stream()
+				.flatMap(e -> e.getRoles().stream())
+				.filter(r -> Set.of(TEACHER, TEACHER_RO, HEAD_TA, TA).contains(r.getType()))
+				.map(RolePersonDetailsDTO::getPerson)
+				.distinct()
+				.toList());
 
 		return "lab/view/" + qSession.getType().name().toLowerCase();
 	}
@@ -723,6 +781,17 @@ public class LabController {
 				return edition.getId();
 			}));
 
+			Set<Long> alreadyInGroup = sgCache.getByPerson(person.getId()).stream()
+					.map(g -> g.getModule().getId()).collect(Collectors.toSet());
+			Set<Long> hasEmptyGroups = qSession.getModules().stream()
+					.filter(m -> !alreadyInGroup.contains(m))
+					.filter(m -> sgApi.getAllGroupsInModule(m).any(g -> g.getMemberUsernames().isEmpty())
+							.block())
+					.collect(Collectors.toSet());
+
+			model.addAttribute("needToJoinGroup",
+					allowedAssignments.stream().filter(a -> hasEmptyGroups.contains(a.getModule().getId()))
+							.map(AssignmentDetailsDTO::getId).collect(Collectors.toSet()));
 			model.addAttribute("assignments", assignments);
 			model.addAttribute("notEnqueueAble", notEnqueueAble);
 			model.addAttribute("types", lab.getAllowedRequests().stream()
@@ -822,4 +891,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/controller/ProfileController.java b/src/main/java/nl/tudelft/queue/controller/ProfileController.java
new file mode 100644
index 0000000000000000000000000000000000000000..21346b26ea28b97687caea52a4b156f5f8c3164f
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/controller/ProfileController.java
@@ -0,0 +1,52 @@
+/*
+ * 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.controller;
+
+import lombok.AllArgsConstructor;
+import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
+import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.queue.dto.patch.ProfilePatchDTO;
+import nl.tudelft.queue.repository.ProfileRepository;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+@AllArgsConstructor
+@RequestMapping("profile")
+@Controller("queueProfileController")
+public class ProfileController {
+
+	private ProfileRepository profileRepository;
+
+	/**
+	 * Updates the user's profile.
+	 *
+	 * @param  person The person whose profile to update
+	 * @param  patch  The patch with new data
+	 * @return        Empty http response
+	 */
+	@ResponseBody
+	@PostMapping("update")
+	public ResponseEntity<Void> updateProfile(@AuthenticatedPerson Person person,
+			@RequestBody ProfilePatchDTO patch) {
+		profileRepository.save(patch.apply(profileRepository.findProfileForPerson(person)));
+		return ResponseEntity.ok().build();
+	}
+
+}
diff --git a/src/main/java/nl/tudelft/queue/controller/RequestController.java b/src/main/java/nl/tudelft/queue/controller/RequestController.java
index 3d427167a590ce89ef0f733a24399fe2e8e89e2e..39b90182c257f5c0cb4e85df47b74742939fc688 100644
--- a/src/main/java/nl/tudelft/queue/controller/RequestController.java
+++ b/src/main/java/nl/tudelft/queue/controller/RequestController.java
@@ -45,6 +45,7 @@ import nl.tudelft.queue.model.Request;
 import nl.tudelft.queue.model.SelectionRequest;
 import nl.tudelft.queue.model.labs.Lab;
 import nl.tudelft.queue.repository.LabRequestRepository;
+import nl.tudelft.queue.repository.ProfileRepository;
 import nl.tudelft.queue.service.*;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -63,6 +64,9 @@ public class RequestController {
 	@Autowired
 	private LabRequestRepository lrr;
 
+	@Autowired
+	private ProfileRepository pr;
+
 	@Autowired
 	private RequestService rs;
 
@@ -173,6 +177,7 @@ public class RequestController {
 			Predicate<LabRequest> requestFilter,
 			boolean reversed, boolean forwardedFirst) {
 		var filter = rts.checkAndStoreFilterDTO(null, filterPath);
+		var language = pr.findProfileForPerson(assistant).getLanguage();
 
 		List<QueueSession<?>> qSessions = rts.addFilterAttributes(model, null);
 		List<Lab> labs = qSessions.stream()
@@ -181,7 +186,7 @@ public class RequestController {
 				.collect(Collectors.toList());
 
 		List<LabRequest> filteredRequests = rs
-				.filterRequestsSharedEditionCheck(lrr.findAllByFilter(labs, filter))
+				.filterRequestsSharedEditionCheck(lrr.findAllByFilter(labs, filter, language))
 				.stream().filter(requestFilter)
 				.toList();
 
diff --git a/src/main/java/nl/tudelft/queue/controller/RoomController.java b/src/main/java/nl/tudelft/queue/controller/RoomController.java
index 7551e5e69a1b1e5f02048b9a945f0ab349bddcff..bb8bb454507a72086ba9057f7ba04073d2606cf0 100644
--- a/src/main/java/nl/tudelft/queue/controller/RoomController.java
+++ b/src/main/java/nl/tudelft/queue/controller/RoomController.java
@@ -17,11 +17,11 @@
  */
 package nl.tudelft.queue.controller;
 
-import java.io.File;
-
-import nl.tudelft.queue.properties.QueueProperties;
+import nl.tudelft.queue.service.AdminService;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.rest.webmvc.ResourceNotFoundException;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.*;
 
@@ -29,22 +29,20 @@ import org.springframework.web.bind.annotation.*;
 public class RoomController {
 
 	@Autowired
-	private QueueProperties qp;
+	private AdminService adminService;
 
 	/**
-	 * Gets the image path to a specific room if it exists.
+	 * Gets the image file for a specific room.
 	 *
-	 * @param  id The id of the room for which the image should be looked up
-	 * @return    The path to the image for the specific room, or nothing if it doesn't exist
+	 * @param  id The id of the room
+	 * @return    A resource with the image
 	 */
-	@GetMapping(value = "/room/map/{id}", produces = "application/json")
-	@ResponseBody
-	public String getRoom(@PathVariable("id") Long id) {
-		String filePath = "/maps/room-" + id;
-		File roomImage = new File(qp.getStaticallyServedPath() + filePath);
-		if (roomImage.exists()) {
-			return "{\"path\": \"" + filePath + "\"}";
-		}
-		return "{}";
+	@PreAuthorize("@permissionService.isAuthenticated()")
+	@GetMapping("/room/map/{id}")
+	public @ResponseBody String getRoomImageFile(@PathVariable("id") Long id) {
+		String fileName = adminService.getRoomFileName(id);
+		if (fileName == null)
+			throw new ResourceNotFoundException();
+		return "{\"fileName\": \"" + fileName + "\"}";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java b/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java
index 1f2cc1fe13efbbad01eca0311012f20d2e8a6637..cd91422d3da755b2a6fef29c93a6290162a0f0fc 100644
--- a/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java
+++ b/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java
@@ -18,6 +18,7 @@
 package nl.tudelft.queue.controller;
 
 import java.time.LocalDateTime;
+import java.util.Comparator;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -97,15 +98,20 @@ public class SharedEditionController {
 				.filter(s -> s.getStart().isBefore(now) && s.getEndTime().isAfter(now))
 				.map(SessionSummaryDTO::getId).toList();
 		var currentSessions = lr.findAllBySessions(currentSessionsIds).stream().map(l -> View.convert(l,
-				QueueSessionSummaryDTO.class)).toList();
+				QueueSessionSummaryDTO.class)).sorted(Comparator.comparing(l -> l.getSlot().getOpensAt()))
+				.toList();
 		var upComingSessionsIds = allSessions.stream().filter(s -> s.getStart().isAfter(now))
 				.map(SessionSummaryDTO::getId).toList();
 		var upComingSessions = lr.findAllBySessions(upComingSessionsIds).stream().map(l -> View.convert(l,
-				QueueSessionSummaryDTO.class)).toList();
+				QueueSessionSummaryDTO.class)).sorted(Comparator.comparing(l -> l.getSlot().getOpensAt()))
+				.toList();
 		var pastSessionsIds = allSessions.stream().filter(s -> s.getEndTime().isBefore(now))
 				.map(SessionSummaryDTO::getId).toList();
 		var pastSessions = lr.findAllBySessions(pastSessionsIds).stream()
-				.map(l -> View.convert(l, QueueSessionSummaryDTO.class)).toList();
+				.map(l -> View.convert(l, QueueSessionSummaryDTO.class))
+				.sorted(Comparator.comparing((QueueSessionSummaryDTO l) -> l.getSlot().getOpensAt())
+						.reversed())
+				.toList();
 
 		var editionTeachers = editions.stream().collect(Collectors.toMap(EditionDetailsDTO::getId,
 				s -> roleDTOService.teacherNames(s)));
diff --git a/src/main/java/nl/tudelft/queue/controller/StudentGroupController.java b/src/main/java/nl/tudelft/queue/controller/StudentGroupController.java
new file mode 100644
index 0000000000000000000000000000000000000000..38cfd9d0d48942295323a6d6cb4ee569d5cee691
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/controller/StudentGroupController.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.controller;
+
+import lombok.AllArgsConstructor;
+import nl.tudelft.labracore.api.StudentGroupControllerApi;
+import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
+import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.queue.cache.ModuleCacheManager;
+import nl.tudelft.queue.cache.StudentGroupCacheManager;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+@AllArgsConstructor
+@RequestMapping("group")
+public class StudentGroupController {
+
+	private ModuleCacheManager mCache;
+	private StudentGroupCacheManager sgCache;
+	private StudentGroupControllerApi sgApi;
+
+	/**
+	 * Joins the specified group.
+	 *
+	 * @param  id The id of the group to join
+	 * @return    Redirects to edition page
+	 */
+	@PostMapping("{id}/join")
+	@PreAuthorize("@permissionService.canJoinGroup(#id)")
+	public String joinGroup(@AuthenticatedPerson Person person, @PathVariable Long id) {
+		sgApi.addMemberToGroup(id, person.getId()).block();
+		return "redirect:/edition/"
+				+ mCache.getRequired(sgCache.getRequired(id).getModule().getId()).getEdition().getId();
+	}
+
+}
diff --git a/src/main/java/nl/tudelft/queue/dialect/ProfileDialect.java b/src/main/java/nl/tudelft/queue/dialect/ProfileDialect.java
new file mode 100644
index 0000000000000000000000000000000000000000..8dcf68940c06f591ca66bf5bef79176d771c80e4
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/dialect/ProfileDialect.java
@@ -0,0 +1,43 @@
+/*
+ * 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.dialect;
+
+import nl.tudelft.queue.repository.ProfileRepository;
+
+import org.thymeleaf.dialect.IExpressionObjectDialect;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+
+public class ProfileDialect implements IExpressionObjectDialect {
+	private static final String NAME = "Profile";
+
+	private final ProfileRepository profileRepository;
+
+	public ProfileDialect(ProfileRepository profileRepository) {
+		this.profileRepository = profileRepository;
+	}
+
+	@Override
+	public IExpressionObjectFactory getExpressionObjectFactory() {
+		return new ProfileExpressionFactory(profileRepository);
+	}
+
+	@Override
+	public String getName() {
+		return NAME;
+	}
+}
diff --git a/src/main/java/nl/tudelft/queue/dialect/ProfileExpressionFactory.java b/src/main/java/nl/tudelft/queue/dialect/ProfileExpressionFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d3cb15a77d5c15ae873603b82050f80c616b4e2
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/dialect/ProfileExpressionFactory.java
@@ -0,0 +1,75 @@
+/*
+ * 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.dialect;
+
+import java.util.Optional;
+import java.util.Set;
+
+import nl.tudelft.labracore.lib.security.LabradorUserDetails;
+import nl.tudelft.queue.repository.ProfileRepository;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.thymeleaf.context.IExpressionContext;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+
+public class ProfileExpressionFactory implements IExpressionObjectFactory {
+	private static final String PROFILE_NAME = "profile";
+
+	private final ProfileRepository profileRepository;
+
+	public ProfileExpressionFactory(ProfileRepository profileRepository) {
+		this.profileRepository = profileRepository;
+	}
+
+	@Override
+	public Set<String> getAllExpressionObjectNames() {
+		return Set.of(PROFILE_NAME);
+	}
+
+	@Override
+	public Object buildObject(IExpressionContext context, String expressionObjectName) {
+		if (PROFILE_NAME.equals(expressionObjectName)) {
+			return getUserDetails()
+					.map(LabradorUserDetails::getUser)
+					.map(profileRepository::findProfileForPerson)
+					.orElse(null);
+		}
+
+		return null;
+	}
+
+	@Override
+	public boolean isCacheable(String expressionObjectName) {
+		return true;
+	}
+
+	private Optional<LabradorUserDetails> getUserDetails() {
+		Authentication auth = SecurityContextHolder.getContext()
+				.getAuthentication();
+		if (auth == null) {
+			return Optional.empty();
+		}
+
+		if (auth.getPrincipal() instanceof LabradorUserDetails) {
+			return Optional.of((LabradorUserDetails) auth.getPrincipal());
+		}
+
+		return Optional.empty();
+	}
+}
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 0000000000000000000000000000000000000000..260bedb228c0522cd284cc4dac90d89f63d141bd
--- /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/create/labs/LabCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java
index 8ddd9ea4af5bea4e65a2d09390ca3d84bac73ff4..feb1bbb996570a84284a97bea83eff800beca3ba 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java
@@ -58,6 +58,8 @@ public abstract class LabCreateDTO<D extends Lab> extends QueueSessionCreateDTO<
 	@Builder.Default
 	private Set<OnlineMode> onlineModes = new HashSet<>();
 
+	@Builder.Default
+	private Boolean isBilingual = false;
 	@Builder.Default
 	private Boolean enableExperimental = false;
 
diff --git a/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java
index e176a56f2bee60fdac0ffaa8661d44cf32ec2d49..a9bc7aa7e33ee63e0e7b36f61f0faa1275901a3a 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java
@@ -24,10 +24,7 @@ import java.util.Objects;
 import javax.validation.constraints.NotNull;
 import javax.validation.constraints.Size;
 
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.NoArgsConstructor;
+import lombok.*;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.queue.cache.AssignmentCacheManager;
 import nl.tudelft.queue.cache.SessionCacheManager;
@@ -35,6 +32,7 @@ import nl.tudelft.queue.dto.create.RequestCreateDTO;
 import nl.tudelft.queue.dto.id.TimeSlotIdDTO;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.model.embeddables.AllowedRequest;
+import nl.tudelft.queue.model.enums.Language;
 import nl.tudelft.queue.model.enums.OnlineMode;
 import nl.tudelft.queue.model.enums.RequestType;
 import nl.tudelft.queue.model.labs.Lab;
@@ -65,6 +63,9 @@ public class LabRequestCreateDTO extends RequestCreateDTO<LabRequest, Lab> {
 
 	private OnlineMode onlineMode;
 
+	@Builder.Default
+	private Language language = Language.ANY;
+
 	private transient Lab session;
 
 	@Override
diff --git a/src/main/java/nl/tudelft/queue/dto/patch/LabPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/LabPatchDTO.java
index abcb220d9ea369ebd5dda648b7bc5ec759546db3..ebf7fea69f5412c0cdf68dec338ab690e677fe86 100644
--- a/src/main/java/nl/tudelft/queue/dto/patch/LabPatchDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/patch/LabPatchDTO.java
@@ -51,6 +51,7 @@ public abstract class LabPatchDTO<D extends Lab> extends QueueSessionPatchDTO<D>
 	private Set<OnlineMode> onlineModes = null;
 
 	private Boolean enableExperimental;
+	private Boolean isBilingual;
 
 	@Override
 	protected void applyOneToOne() {
@@ -61,6 +62,7 @@ public abstract class LabPatchDTO<D extends Lab> extends QueueSessionPatchDTO<D>
 		}
 		updateNonNull(onlineModes, data::setOnlineModes);
 		data.setEnableExperimental(Boolean.TRUE.equals(enableExperimental)); // null is false as well
+		data.setIsBilingual(Boolean.TRUE.equals(isBilingual));
 	}
 
 	@Override
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 0000000000000000000000000000000000000000..40f18e4ba476807de4bed17753789b50450dd86b
--- /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/dto/patch/ProfilePatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/ProfilePatchDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9aa457c69b62de71c304a7febf472d123870410
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/dto/patch/ProfilePatchDTO.java
@@ -0,0 +1,44 @@
+/*
+ * 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 lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import nl.tudelft.librador.dto.patch.Patch;
+import nl.tudelft.queue.model.Profile;
+import nl.tudelft.queue.model.enums.Language;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProfilePatchDTO extends Patch<Profile> {
+
+	private Language language;
+
+	@Override
+	protected void applyOneToOne() {
+		updateNonNull(language, data::setLanguage);
+	}
+
+	@Override
+	protected void validate() {
+	}
+}
diff --git a/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java
index 60ea91065092e043bf36c000ae2dcadf5af01d9d..36587ef9b81cb1686adcebaef73c244a0bfb2e97 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java
@@ -38,4 +38,6 @@ public abstract class LabViewDTO<D extends Lab> extends QueueSessionViewDTO<D> {
 
 	private Set<OnlineMode> onlineModes;
 
+	private Boolean isBilingual;
+
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java
index 9a4efc940b2194e338022daef0840004252e0324..92c6d654337618690242080a3f36ea70ba44cb68 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java
@@ -35,6 +35,7 @@ import nl.tudelft.queue.dto.view.events.RequestHandledEventViewDTO;
 import nl.tudelft.queue.model.Feedback;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.model.TimeSlot;
+import nl.tudelft.queue.model.enums.Language;
 import nl.tudelft.queue.model.enums.OnlineMode;
 import nl.tudelft.queue.model.enums.RequestType;
 
@@ -54,6 +55,8 @@ public class LabRequestViewDTO extends RequestViewDTO<LabRequest> {
 	private TimeSlot timeSlot;
 	private OnlineMode onlineMode;
 
+	private Language language;
+
 	private RequestType requestType;
 
 	private AssignmentDetailsDTO assignment;
diff --git a/src/main/java/nl/tudelft/queue/model/LabRequest.java b/src/main/java/nl/tudelft/queue/model/LabRequest.java
index c87900bab31fa17de1b4a4839c707f049b5d40db..0a522cd09d1d3c1c99dc9d29c8611ca86c824d5f 100644
--- a/src/main/java/nl/tudelft/queue/model/LabRequest.java
+++ b/src/main/java/nl/tudelft/queue/model/LabRequest.java
@@ -33,6 +33,7 @@ import lombok.*;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.librador.dto.view.View;
 import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO;
+import nl.tudelft.queue.model.enums.Language;
 import nl.tudelft.queue.model.enums.OnlineMode;
 import nl.tudelft.queue.model.enums.RequestType;
 import nl.tudelft.queue.model.labs.ExamLab;
@@ -80,6 +81,14 @@ public class LabRequest extends Request<Lab> {
 	 */
 	private OnlineMode onlineMode;
 
+	/**
+	 * The language of the request.
+	 */
+	@NotNull
+	@Builder.Default
+	@Enumerated(EnumType.STRING)
+	private Language language = Language.ANY;
+
 	/**
 	 * The timeslot this request occupies. Timeslots can be used as opposed to direct requests to allow for
 	 * reserving a TA for a specific time ahead of the lab starting.
diff --git a/src/main/java/nl/tudelft/queue/model/Profile.java b/src/main/java/nl/tudelft/queue/model/Profile.java
new file mode 100644
index 0000000000000000000000000000000000000000..cf9ce3f1bc9397d1efbcfa2b13e42fd95d8e32bc
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/model/Profile.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.validation.constraints.NotNull;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import nl.tudelft.queue.model.enums.Language;
+
+@Data
+@Entity
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Profile {
+
+	@Id
+	private Long id;
+
+	@NotNull
+	@Builder.Default
+	@Enumerated(EnumType.STRING)
+	private Language language = Language.ENGLISH_ONLY;
+
+}
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 0000000000000000000000000000000000000000..9b267d1ed57cdb7fd6ce9b10dffdceabeb959715
--- /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/enums/Language.java b/src/main/java/nl/tudelft/queue/model/enums/Language.java
new file mode 100644
index 0000000000000000000000000000000000000000..cdb578d60e4a6742d622980e190f2188c6be973f
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/model/enums/Language.java
@@ -0,0 +1,24 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.model.enums;
+
+public enum Language {
+
+	DUTCH_ONLY, ENGLISH_ONLY, ANY;
+
+}
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 73972386db8f1891823b222f71f20a589cb1e86d..ab0c9e4062013357de6cead9bb2fce6ffbe09773 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;
@@ -90,10 +88,19 @@ public abstract class Lab extends QueueSession<LabRequest> {
 	@ElementCollection
 	private Set<OnlineMode> onlineModes = new HashSet<>();
 
+	@NotNull
+	@Builder.Default
+	private Boolean isBilingual = false;
+
 	@NotNull
 	@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 0000000000000000000000000000000000000000..f59ac7c0d58cd01cd017b36fdca04602f1d89d59
--- /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 0000000000000000000000000000000000000000..e353965b4592a3cc3e7669153aedcadee49d5eb7
--- /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 0000000000000000000000000000000000000000..2a0dba2388669e0def373de8055da4f38e283427
--- /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/LabRequestRepository.java b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java
index 404129d6569fe41d42078d82dee9ea2b0398fac0..556fdd0ff147ded47452390c61a0e822ce1385ba 100644
--- a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java
+++ b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java
@@ -30,6 +30,7 @@ import nl.tudelft.labracore.api.dto.AssignmentSummaryDTO;
 import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
 import nl.tudelft.queue.model.*;
+import nl.tudelft.queue.model.enums.Language;
 import nl.tudelft.queue.model.enums.QueueSessionType;
 import nl.tudelft.queue.model.enums.RequestStatus;
 import nl.tudelft.queue.model.labs.AbstractSlottedLab;
@@ -197,15 +198,17 @@ public interface LabRequestRepository
 	 * @param  assistant          The assistant that is getting the next request.
 	 * @param  filter             The filter to apply to the request table query.
 	 * @param  allowedAssignments The assignments which the assistant is allowed to get.
+	 * @param  language           The assistant's language choice
 	 * @return                    The next slotted request from the given lab.
 	 */
 	default Optional<LabRequest> findNextSlotRequest(AbstractSlottedLab<?> lab, Person assistant,
-			RequestTableFilterDTO filter, List<AssignmentSummaryDTO> allowedAssignments) {
+			RequestTableFilterDTO filter, List<AssignmentSummaryDTO> allowedAssignments, Language language) {
 		List<Long> assignments = allowedAssignments.stream().map(AssignmentSummaryDTO::getId)
 				.collect(Collectors.toList());
 		return findAll(createFilterBooleanExpression(filter,
 				qlr.session.id.eq(lab.getId())
 						.and(qlr.assignment.in(assignments))
+						.and(matchesLanguagePreference(language))
 						.and(isStatusOrForwardedToAny(RequestStatus.PENDING, assistant))
 						.and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(lab.getEarlyOpenTime())))),
 				PageRequest.of(0, 1, Sort.sort(LabRequest.class)
@@ -222,14 +225,16 @@ public interface LabRequestRepository
 	 * @param  assistant          The assistant requesting the number of open requests.
 	 * @param  filter             The fitler the assistant is applying to their requests view.
 	 * @param  allowedAssignments The assignments which the assistant is allowed to get.
+	 * @param  language           The assistant's language choice
 	 * @return                    The count of open slotted requests.
 	 */
 	default long countOpenSlotRequests(AbstractSlottedLab<?> lab, Person assistant,
-			RequestTableFilterDTO filter, List<AssignmentSummaryDTO> allowedAssignments) {
+			RequestTableFilterDTO filter, List<AssignmentSummaryDTO> allowedAssignments, Language language) {
 		var assignments = allowedAssignments.stream().map(AssignmentSummaryDTO::getId).toList();
 		return count(createFilterBooleanExpression(filter,
 				qlr.session.id.eq(lab.getId())
 						.and(qlr.assignment.in(assignments))
+						.and(matchesLanguagePreference(language))
 						.and(isStatusOrForwarded(RequestStatus.PENDING, assistant))
 						.and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(lab.getEarlyOpenTime())))));
 	}
@@ -242,16 +247,19 @@ public interface LabRequestRepository
 	 * @param  assistant          The assistant that is getting the next request.
 	 * @param  filter             The filter to apply to the request table query.
 	 * @param  allowedAssignments The assignments which the assistant is allowed to get.
+	 * @param  language           The assistant's language choice
 	 * @return                    The next request from the given lab.
 	 */
 	default Optional<LabRequest> findNextNormalRequest(Lab lab, Person assistant,
 			RequestTableFilterDTO filter,
-			List<AssignmentSummaryDTO> allowedAssignments) {
+			List<AssignmentSummaryDTO> allowedAssignments,
+			Language language) {
 		List<Long> assignments = allowedAssignments.stream().map(AssignmentSummaryDTO::getId)
 				.collect(Collectors.toList());
 		return findAll(createFilterBooleanExpression(filter,
 				qlr.session.id.eq(lab.getId())
 						.and(isStatusOrForwardedToAny(RequestStatus.PENDING, assistant))
+						.and(matchesLanguagePreference(language))
 						.and(qlr.assignment.in(assignments))),
 				PageRequest.of(0, 1, Sort.sort(LabRequest.class).by(LabRequest::getCreatedAt).ascending()))
 						.stream().findFirst();
@@ -265,17 +273,36 @@ public interface LabRequestRepository
 	 * @param  assistant          The assistant requesting this count.
 	 * @param  filter             The filter applied by the assistant on the page they are requesting.
 	 * @param  allowedAssignments The assignments which the assistant is allowed to get.
+	 * @param  language           The assistant's language choice
 	 * @return                    The number of regular requests in the given lab.
 	 */
 	default long countNormalRequests(Lab lab, Person assistant, RequestTableFilterDTO filter,
-			List<AssignmentSummaryDTO> allowedAssignments) {
+			List<AssignmentSummaryDTO> allowedAssignments, Language language) {
 		var assignemnts = allowedAssignments.stream().map(AssignmentSummaryDTO::getId).toList();
 		return count(createFilterBooleanExpression(filter,
 				qlr.session.id.eq(lab.getId())
 						.and(qlr.assignment.in(assignemnts))
+						.and(matchesLanguagePreference(language))
 						.and(isStatusOrForwarded(RequestStatus.PENDING, assistant))));
 	}
 
+	/**
+	 * Checks whether the request matches a given language preference. Bilingual requests match every
+	 * preference. English only and dutch only match their respective preferences as well as the bilingual
+	 * preference.
+	 *
+	 * @param  preference The language preference
+	 * @return            The predicate for matching the language preference
+	 */
+	default BooleanExpression matchesLanguagePreference(Language preference) {
+		return switch (preference) {
+			case ANY -> qlr.language.isNotNull(); // Always true but Expressions.TRUE does not work
+			case DUTCH_ONLY -> qlr.language.eq(Language.ANY).or(qlr.language.eq(Language.DUTCH_ONLY));
+			case ENGLISH_ONLY -> qlr.language.eq(Language.ANY)
+					.or(qlr.language.eq(Language.ENGLISH_ONLY));
+		};
+	}
+
 	/**
 	 * Creates a {@link BooleanExpression} for querying the request table. This expression checks whether the
 	 * request either has the given status or the forwarded status with no specific person it is forwarded to.
@@ -330,17 +357,20 @@ public interface LabRequestRepository
 	 * requesting to see these requests. The filter contains all labs, rooms, assignments, etc. that need to
 	 * be displayed. If a field in the filter is left as an empty set, the filter ignores it.
 	 *
-	 * @param  labs   The labs that the should also be kept in the filter.
-	 * @param  filter The filter to apply to the boolean expression.
-	 * @return        The filtered list of requests.
+	 * @param  labs     The labs that the should also be kept in the filter.
+	 * @param  filter   The filter to apply to the boolean expression.
+	 * @param  language The assistant's language choice
+	 * @return          The filtered list of requests.
 	 */
-	default List<LabRequest> findAllByFilter(List<Lab> labs, RequestTableFilterDTO filter) {
+	default List<LabRequest> findAllByFilter(List<Lab> labs, RequestTableFilterDTO filter,
+			Language language) {
 		return findAll(qlr.in(select(qlr).from(qlr)
 				.leftJoin(qlr.timeSlot, QTimeSlot.timeSlot).on(qlr.timeSlot.id.eq(QTimeSlot.timeSlot.id))
 				.where(createFilterBooleanExpression(filter, qlr.session.in(labs).and(
 						qlr.session.type.eq(QueueSessionType.REGULAR)
 								.or(qlr.session.type.in(QueueSessionType.SLOTTED, QueueSessionType.EXAM)
-										.and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10)))))))));
+										.and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10))))))
+												.and(matchesLanguagePreference(language)))));
 	}
 
 	/**
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 0000000000000000000000000000000000000000..0d4b7c3b866046a8593227fc89893ac3b4881f42
--- /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/repository/ProfileRepository.java b/src/main/java/nl/tudelft/queue/repository/ProfileRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..26303eb461716b8aa4a6d38fc40d61f985441c91
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/repository/ProfileRepository.java
@@ -0,0 +1,36 @@
+/*
+ * 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.labracore.lib.security.user.Person;
+import nl.tudelft.queue.model.Profile;
+
+public interface ProfileRepository extends QueueRepository<Profile, Long> {
+
+	/**
+	 * Finds the profile belonging to the specified person. If the person does not have a profile yet, creates
+	 * a default one.
+	 *
+	 * @param  person The person whose profile to find
+	 * @return        The profile belonging to the person
+	 */
+	default Profile findProfileForPerson(Person person) {
+		return findById(person.getId()).orElseGet(() -> save(Profile.builder().id(person.getId()).build()));
+	}
+
+}
diff --git a/src/main/java/nl/tudelft/queue/security/DevSecurityConfig.java b/src/main/java/nl/tudelft/queue/security/DevSecurityConfig.java
index 46111371a10d5313bbb3bf8f83633ebccbcafcbf..1f38e0fd7edd3109b09f8302706d788532fd68ef 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 5700c640c634127f7ad9bbceae72fab3222de377..7b3c068401afe821cfe15f4b735f10c9c6829ce6 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/AdminService.java b/src/main/java/nl/tudelft/queue/service/AdminService.java
index 4d0b6f7872b1167c489e993c2b779cb06d3138f0..0ee31f8baa00865d841fb26782526691d158ce9c 100644
--- a/src/main/java/nl/tudelft/queue/service/AdminService.java
+++ b/src/main/java/nl/tudelft/queue/service/AdminService.java
@@ -23,6 +23,11 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
+import java.util.Collections;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import nl.tudelft.queue.properties.QueueProperties;
 
@@ -55,4 +60,43 @@ public class AdminService {
 		Files.copy(file.getInputStream(), savedFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
 	}
 
+	/**
+	 * Gets the filename of a room map.
+	 *
+	 * @return The filename corresponding to the map of the given room.
+	 */
+	public String getRoomFileName(Long roomId) {
+		Path mapStoragePath = Paths.get(qp.getStaticallyServedPath(), "maps");
+		try (var files = Files.walk(mapStoragePath, 1)) {
+			return files
+					.map(p -> p.getFileName().toString())
+					.filter(name -> name.matches("room-" + roomId + "\\.\\w+"))
+					.findFirst().orElse(null);
+		} catch (IOException e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+
+	/**
+	 * Gets all rooms that have a map stored.
+	 *
+	 * @return The set of room IDs with a map
+	 */
+	public Set<Long> getRoomsWithMaps() {
+		Path mapStoragePath = Paths.get(qp.getStaticallyServedPath(), "maps");
+		Pattern pattern = Pattern.compile("room-(\\d+)\\.\\w+");
+		try (var files = Files.walk(mapStoragePath, 1)) {
+			return files
+					.map(p -> p.getFileName().toString())
+					.map(pattern::matcher)
+					.filter(Matcher::matches)
+					.map(matcher -> Long.parseLong(matcher.group(1)))
+					.collect(Collectors.toSet());
+		} catch (IOException e) {
+			e.printStackTrace();
+			return Collections.emptySet();
+		}
+	}
+
 }
diff --git a/src/main/java/nl/tudelft/queue/service/EditionService.java b/src/main/java/nl/tudelft/queue/service/EditionService.java
index 64a48bb7c3db34de442c668d840ce7380235d82b..05054977813b060517084bd6b47b216342d5d18e 100644
--- a/src/main/java/nl/tudelft/queue/service/EditionService.java
+++ b/src/main/java/nl/tudelft/queue/service/EditionService.java
@@ -25,6 +25,7 @@ import java.io.IOException;
 import java.util.*;
 import java.util.List;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
@@ -37,6 +38,7 @@ import nl.tudelft.labracore.api.PersonControllerApi;
 import nl.tudelft.labracore.api.RoleControllerApi;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.queue.cache.AssignmentCacheManager;
+import nl.tudelft.queue.cache.CourseCacheManager;
 import nl.tudelft.queue.csv.CsvHelper;
 import nl.tudelft.queue.csv.EmptyCsvException;
 import nl.tudelft.queue.csv.InvalidCsvException;
@@ -96,6 +98,9 @@ public class EditionService {
 	@Autowired
 	private AssignmentCacheManager acm;
 
+	@Autowired
+	private CourseCacheManager cCache;
+
 	/**
 	 * Converts any kind of EditionDTO to a QueueEditionDTO.
 	 *
@@ -124,6 +129,34 @@ public class EditionService {
 		}).toList();
 	}
 
+	/**
+	 * Filters a list of editions by an edition filter.
+	 *
+	 * @param  editions The original list of editions
+	 * @param  filter   The filter to apply
+	 * @return          The filtered list of editions
+	 */
+	public List<EditionDetailsDTO> filterEditions(List<EditionDetailsDTO> editions, EditionFilterDTO filter) {
+		Stream<EditionDetailsDTO> filtered = editions.stream();
+
+		if (filter.getPrograms() != null && !filter.getPrograms().isEmpty()) {
+			Set<Long> programFilter = new HashSet<>(filter.getPrograms());
+			cCache.getAndIgnoreMissing(editions.stream().map(e -> e.getCourse().getId()).distinct());
+			filtered = filtered
+					.filter(e -> programFilter
+							.contains(cCache.getRequired(e.getCourse().getId()).getProgram().getId()));
+		}
+
+		if (filter.getNameSearch() != null && !filter.getNameSearch().isBlank()) {
+			String lcNameFilter = filter.getNameSearch().toLowerCase();
+			filtered = filtered.filter(e -> e.getName().toLowerCase().contains(lcNameFilter) ||
+					e.getCourse().getName().toLowerCase().contains(lcNameFilter) ||
+					e.getCourse().getCode().toLowerCase().contains(lcNameFilter));
+		}
+
+		return filtered.toList();
+	}
+
 	/**
 	 * Gets a queue edition from the database or creates a default one if it does not exist;
 	 *
diff --git a/src/main/java/nl/tudelft/queue/service/LabService.java b/src/main/java/nl/tudelft/queue/service/LabService.java
index 1111119208d16f71b2577ecc55c9eca2be23b145..3c65d220698a23cb0b33a39b9471f2d0f5abab2f 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/java/nl/tudelft/queue/service/PermissionService.java b/src/main/java/nl/tudelft/queue/service/PermissionService.java
index fbf8c7d38b9f32eb91616e235663695ad7c5947f..7fb6c4d32890ea5fa4349dd977af09e9100ed50f 100644
--- a/src/main/java/nl/tudelft/queue/service/PermissionService.java
+++ b/src/main/java/nl/tudelft/queue/service/PermissionService.java
@@ -641,7 +641,8 @@ public class PermissionService {
 						editions -> withAnyRole(editions, (person, role) -> {
 							switch (role) {
 								case TEACHER:
-									return !request.getEventInfo().getStatus().isHandled();
+									return !request.getEventInfo().getStatus().isHandled()
+											&& request.getEventInfo().getStatus() != RequestStatus.REVOKED;
 								case HEAD_TA:
 								case TA:
 									return Objects
@@ -783,4 +784,17 @@ public class PermissionService {
 						.anyMatch(m -> m.getId().equals(person.getId()))
 						|| isAdmin());
 	}
+
+	/**
+	 * Checks if the authenticated user can join a given group.
+	 *
+	 * @param  groupId The id of the group to join
+	 * @return         True iff the user can join the group
+	 */
+	public boolean canJoinGroup(long groupId) {
+		StudentGroupDetailsDTO group = sgCache.getRequired(groupId);
+		ModuleDetailsDTO module = mCache.getRequired(group.getModule().getId());
+		return withAuthenticatedUser(person -> withRole(module.getEdition().getId(), person.getId(),
+				role -> group.getMembers().size() < group.getCapacity()));
+	}
 }
diff --git a/src/main/java/nl/tudelft/queue/service/RequestService.java b/src/main/java/nl/tudelft/queue/service/RequestService.java
index c68bbf33e17211730bcb09262bd7b135e100f4f8..013a04f2ce2615a3110a8ba49306e96611e89452 100644
--- a/src/main/java/nl/tudelft/queue/service/RequestService.java
+++ b/src/main/java/nl/tudelft/queue/service/RequestService.java
@@ -46,6 +46,7 @@ import nl.tudelft.queue.model.labs.ExamLab;
 import nl.tudelft.queue.model.labs.Lab;
 import nl.tudelft.queue.model.labs.SlottedLab;
 import nl.tudelft.queue.repository.LabRequestRepository;
+import nl.tudelft.queue.repository.ProfileRepository;
 import nl.tudelft.queue.repository.RequestEventRepository;
 import nl.tudelft.queue.repository.RequestRepository;
 
@@ -68,6 +69,9 @@ public class RequestService {
 	@Autowired
 	private RequestEventRepository rer;
 
+	@Autowired
+	private ProfileRepository pr;
+
 	@Autowired
 	private QueuePushService qps;
 
@@ -237,7 +241,7 @@ public class RequestService {
 		if (answer != null && !answer.isBlank()) {
 			qApi.patchQuestion(new QuestionPatchDTO().answer(answer), request.getQuestionId()).block();
 		}
-		wss.sendRequestHandled(event);
+		wss.sendRequestHandled(request, event);
 	}
 
 	/**
@@ -254,7 +258,7 @@ public class RequestService {
 			String reasonForStudent) {
 		var event = rer.applyAndSave(
 				new RequestRejectedEvent(request, assistant, reasonForAssistant, reasonForStudent));
-		wss.sendRequestHandled(event);
+		wss.sendRequestHandled(request, event);
 	}
 
 	/**
@@ -450,9 +454,10 @@ public class RequestService {
 	 */
 	private Optional<LabRequest> findNextRequest(Lab lab, Person assistant, RequestTableFilterDTO filter) {
 		var allowedAssignments = lService.getAllowedAssignmentsInLab(lab, assistant);
+		var language = pr.findProfileForPerson(assistant).getLanguage();
 		if (lab instanceof SlottedLab) {
 			return lrr.findNextSlotRequest((AbstractSlottedLab<?>) lab, assistant, filter,
-					allowedAssignments);
+					allowedAssignments, language);
 		} else if (lab instanceof ExamLab) {
 			ExamLab examLab = (ExamLab) lab;
 			return examLab.getTimeSlots().stream().filter(ClosableTimeSlot::getActive)
@@ -460,7 +465,7 @@ public class RequestService {
 							.stream())
 					.findFirst();
 		} else {
-			return lrr.findNextNormalRequest(lab, assistant, filter, allowedAssignments);
+			return lrr.findNextNormalRequest(lab, assistant, filter, allowedAssignments, language);
 		}
 	}
 
diff --git a/src/main/java/nl/tudelft/queue/service/RequestTableService.java b/src/main/java/nl/tudelft/queue/service/RequestTableService.java
index 7254cac658101827360d7c65674209b8ba86b87a..58ebdf0be90f91c9e848caa741e39e8cad52cc5d 100644
--- a/src/main/java/nl/tudelft/queue/service/RequestTableService.java
+++ b/src/main/java/nl/tudelft/queue/service/RequestTableService.java
@@ -45,6 +45,7 @@ import nl.tudelft.queue.model.labs.AbstractSlottedLab;
 import nl.tudelft.queue.model.labs.Lab;
 import nl.tudelft.queue.repository.LabRepository;
 import nl.tudelft.queue.repository.LabRequestRepository;
+import nl.tudelft.queue.repository.ProfileRepository;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
@@ -67,6 +68,9 @@ public class RequestTableService {
 	@Autowired
 	private LabRequestRepository rr;
 
+	@Autowired
+	private ProfileRepository pr;
+
 	@Autowired
 	private EditionControllerApi eApi;
 
@@ -159,6 +163,7 @@ public class RequestTableService {
 	 */
 	public Map<Long, Long> labRequestCounts(List<Lab> labs, Person assistant,
 			RequestTableFilterDTO filter) {
+		var language = pr.findProfileForPerson(assistant).getLanguage();
 		return labs.stream()
 				.collect(Collectors.toMap(
 						QueueSession::getId,
@@ -167,10 +172,11 @@ public class RequestTableService {
 									assistant);
 							if (l instanceof AbstractSlottedLab<?>) {
 								return rr.countOpenSlotRequests((AbstractSlottedLab<?>) l,
-										assistant, filter, allowedAssignments);
+										assistant, filter, allowedAssignments, language);
 								// TODO: count exam lab slots
 							} else {
-								return rr.countNormalRequests(l, assistant, filter, allowedAssignments);
+								return rr.countNormalRequests(l, assistant, filter, allowedAssignments,
+										language);
 							}
 						}));
 	}
diff --git a/src/main/java/nl/tudelft/queue/service/WebSocketService.java b/src/main/java/nl/tudelft/queue/service/WebSocketService.java
index 7d6f840f821de638736dac99d9d0fc33f81f58ee..f85989c4cc9f0f969c83cc868acfe54212686c94 100644
--- a/src/main/java/nl/tudelft/queue/service/WebSocketService.java
+++ b/src/main/java/nl/tudelft/queue/service/WebSocketService.java
@@ -259,9 +259,15 @@ public class WebSocketService {
 	 *
 	 * @param event The event that occurred.
 	 */
-	public void sendRequestHandled(RequestHandledEvent event) {
-		sendRequestTableMessage(event.getRequest(),
-				View.convert(event, RequestFinishedMessage.class).withStatus(event.status()));
+	public void sendRequestHandled(LabRequest request, RequestHandledEvent event) {
+		RequestFinishedMessage message = View.convert(event, RequestFinishedMessage.class)
+				.withStatus(event.status());
+		sendRequestTableMessage(event.getRequest(), message);
+		sendMessage(
+				sgApi.getStudentGroupsById(List.of(request.getStudentGroup())).blockFirst()
+						.getMembers().stream()
+						.map(RolePersonLayer1DTO::getPerson),
+				"/topic/lab/" + request.getSession().getId() + "/position", message);
 	}
 
 }
diff --git a/src/main/resources/migrations.yml b/src/main/resources/migrations.yml
index eb73594fccaa2d94dd95d572a96897f929791a03..84ef1a7161249667defef929a2bd7da3f5c7edb4 100644
--- a/src/main/resources/migrations.yml
+++ b/src/main/resources/migrations.yml
@@ -1067,4 +1067,171 @@ databaseChangeLog:
                   defaultValueBoolean: true
             tableName: queue_edition
 
+  # Bilingual labs
+  - changeSet:
+      id: 1693314685096-1
+      author: ruben (generated)
+      changes:
+        - createTable:
+            columns:
+              - column:
+                  constraints:
+                    nullable: false
+                    primaryKey: true
+                    primaryKeyName: CONSTRAINT_E
+                  name: id
+                  type: BIGINT
+              - column:
+                  constraints:
+                    nullable: false
+                  name: language
+                  type: VARCHAR(255)
+                  defaultValue: "ANY"
+            tableName: profile
+  - changeSet:
+      id: 1693314685096-2
+      author: ruben (generated)
+      changes:
+        - addColumn:
+            columns:
+              - column:
+                  constraints:
+                    nullable: false
+                  name: language
+                  type: VARCHAR(255)
+                  defaultValue: "ANY"
+            tableName: lab_request
+  - changeSet:
+      id: 1693314685096-3
+      author: ruben (generated)
+      changes:
+        - addColumn:
+            columns:
+              - column:
+                  constraints:
+                    nullable: false
+                  name: is_bilingual
+                  type: BOOLEAN
+                  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 0000000000000000000000000000000000000000..263b08d2bc7f277e7937d4ddd58c825499280530
--- /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/static/js/create_tab.js b/src/main/resources/static/js/create_tab.js
deleted file mode 100644
index e8a5bba2a4b94d5bfd078df6ca0aa8bc355f1e96..0000000000000000000000000000000000000000
--- a/src/main/resources/static/js/create_tab.js
+++ /dev/null
@@ -1,9 +0,0 @@
-function switchTab(to) {
-    if (to !== currentTab) {
-        document.getElementById(to).classList.toggle("hidden");
-        document.getElementById(currentTab).classList.toggle("hidden");
-        document.getElementById(to + "-tab").classList.toggle("active");
-        document.getElementById(currentTab + "-tab").classList.toggle("active");
-    }
-    currentTab = to;
-}
diff --git a/src/main/resources/static/js/global.js b/src/main/resources/static/js/global.js
index db28a8b38a4f60258dd382d8b198c076008eed2c..8cc70d709067c3193dc8aa29bfd452f3732c3713 100644
--- a/src/main/resources/static/js/global.js
+++ b/src/main/resources/static/js/global.js
@@ -222,11 +222,3 @@ function countChar(val) {
         $("#charCount").text(len + "/500");
     }
 }
-
-function showDialog(id) {
-    document.getElementById(id).showModal();
-}
-
-function hideDialog(id) {
-    document.getElementById(id).close();
-}
diff --git a/src/main/resources/static/js/lab_view.js b/src/main/resources/static/js/lab_view.js
index 64bf2071ea1098e9837e15678bb31bc35e24b117..a471fdcddb3648309c6656a8805eef1715044472 100644
--- a/src/main/resources/static/js/lab_view.js
+++ b/src/main/resources/static/js/lab_view.js
@@ -28,7 +28,13 @@ function handleSocketCreation(client) {
         if (event["type"] === "position-update") {
             $("#position").text(event["position"]);
         } else if (event["type"] === "request-taken") {
-            location.reload();
+            $("#position").text("0");
+            new Notification("You are in front of the Queue");
+            toast("You are in front of the queue", "info");
+        } else if (event["type"] === "request-finished") {
+            const url = new URL(window.location.href);
+            url.searchParams.set("requestFinished", event.id);
+            window.location = url.toString();
         }
     });
 }
diff --git a/src/main/resources/static/js/map_loader.js b/src/main/resources/static/js/map_loader.js
index fefb6e6a44887bc1f91906405fa5b325e803726f..56fb524976ee03785cabe204a1aaabecc5eeb65f 100644
--- a/src/main/resources/static/js/map_loader.js
+++ b/src/main/resources/static/js/map_loader.js
@@ -16,30 +16,24 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 $(() => {
-    $("#inputRoom").change(() => {
+    $("#input-room").change(() => {
         // Get the room ID of the currently selected room.
-        const roomId = $("#inputRoom").find(":selected").attr("value");
+        const roomId = $("#input-room").find(":selected").attr("value");
         updateRequestInfo(roomId);
     });
 });
 
 function updateRequestInfo(roomId) {
+    const imageHolder = document.getElementById("image-holder");
+    const image = imageHolder.querySelector("img");
     $.get({
         url: "/room/map/" + roomId,
         success: function (response) {
-            if (response.path === undefined || response.path === null) {
-                this.error();
-                return;
-            }
-            const image = $('<img class="img-fluid" alt="Room map">');
-            image.attr("src", response.path);
-
-            $("#image-holder").show().html(image);
-            $("#inputLocation").prop("disabled", false);
+            imageHolder.removeAttribute("hidden");
+            image.setAttribute("src", `/maps/${JSON.parse(response).fileName}`);
         },
         error: function () {
-            $("#image-holder").hide();
-            $("#inputLocation").prop("disabled", true);
+            imageHolder.setAttribute("hidden", "");
         },
     });
 }
diff --git a/src/main/resources/static/js/request_table.js b/src/main/resources/static/js/request_table.js
index e26ff9e66fa2b8b34d795736ca018ffcc484587e..53844fe13cd17d93f184906705d675f66aa50d29 100644
--- a/src/main/resources/static/js/request_table.js
+++ b/src/main/resources/static/js/request_table.js
@@ -135,6 +135,7 @@ function prependToRequestTable(event) {
 
     // Add the compiled HTML to the request table with a fade in effect.
     $(html).hide().prependTo("#request-table tbody").fadeIn();
+    addEvents(event.id);
 
     $("#no-requests-info").hide();
 }
@@ -152,10 +153,22 @@ function appendToRequestTable(event) {
 
     // Add the compiled HTML to the request table with a fade in effect.
     $(html).hide().appendTo("#request-table tbody").fadeIn();
+    addEvents(event.id);
 
     $("#no-requests-info").hide();
 }
 
+function addEvents(requestId) {
+    document.getElementById(`request-${requestId}`).addEventListener("click", function (event) {
+        if (["a", "button"].includes(event.target.tagName.toLowerCase())) return;
+        window.location = `/request/${requestId}`;
+    });
+    document.getElementById(`request-${requestId}`).addEventListener("keydown", function (event) {
+        if (event.key !== "Enter" || ["a", "button"].includes(event.target.tagName.toLowerCase())) return;
+        window.location = `/request/${requestId}`;
+    });
+}
+
 /**
  * Removed a request from the request table.
  * @param id {number} The id of the request to remove.
@@ -216,7 +229,7 @@ function decreaseGetNextCounter(labId) {
         counter = counter - 1;
         span.text(`(${counter})`);
         if (counter === 0) {
-            lab.addClass("disabled");
+            lab.attr("disabled", "");
             span.hide();
         }
     }
@@ -231,7 +244,7 @@ function increaseGetNextCounter(labId) {
     const span = $(`#span-${labId}`);
     const count = parseInt(span.text().match(/\d+/)[0]) + 1;
 
-    lab.removeClass("disabled");
+    lab.removeAttr("disabled");
     span.text(`(${count})`);
     span.show();
 }
diff --git a/src/main/resources/templates/admin/view/rooms.html b/src/main/resources/templates/admin/view/rooms.html
index 1023a2c93b3b0b03c31f47fbe3db91cc7906a111..a9f67b53a82211c22a7991d33671a9ffeb3bd575 100644
--- a/src/main/resources/templates/admin/view/rooms.html
+++ b/src/main/resources/templates/admin/view/rooms.html
@@ -23,17 +23,35 @@
 
     <body>
         <section layout:fragment="subcontent">
-            <ul class="surface list divided" role="list">
+            <ul class="surface list divided" role="list" th:with="roomsWithMaps = ${@adminService.roomsWithMaps}">
                 <li class="pbl-3 flex align-center space-between" th:each="room : ${rooms}">
                     <span th:text="${room.name}"></span>
-                    <form class="flex" th:action="@{/admin/room/{id}(id=${room.id})}" method="post" enctype="multipart/form-data">
-                        <input type="file" id="map" name="map" accept="image/*" />
-                        <button type="submit" class="button p-min" data-style="outlined">Upload</button>
-                    </form>
+                    <div class="flex">
+                        <a
+                            th:if="${roomsWithMaps.contains(room.id)}"
+                            th:data-room-id="${room.id}"
+                            type="submit"
+                            data-style="outlined"
+                            class="button p-min"
+                            target="_blank">
+                            Download
+                        </a>
+                        <form class="flex" th:action="@{/admin/room/{id}(id=${room.id})}" method="post" enctype="multipart/form-data">
+                            <input type="file" id="map" name="map" accept="image/*" />
+                            <button type="submit" class="button p-min" data-style="outlined">Upload</button>
+                        </form>
+                    </div>
                 </li>
             </ul>
-            <!-- Invisible column for spacing at the bottom of the page -->
-            <div class="row mt-3"></div>
+            <script>
+                document.addEventListener("DOMContentLoaded", function () {
+                    document.querySelectorAll("[data-room-id]").forEach(link => {
+                        fetch(`/room/map/${link.dataset.roomId}`)
+                            .then(res => res.json())
+                            .then(file => link.setAttribute("href", `/maps/${file.fileName}`));
+                    });
+                });
+            </script>
         </section>
     </body>
 </html>
diff --git a/src/main/resources/templates/edition/view.html b/src/main/resources/templates/edition/view.html
index 4aeb39615ce49579094104f3e348f80827140372..6ea362b85c67576106aa70384bc9079378b6f16a 100644
--- a/src/main/resources/templates/edition/view.html
+++ b/src/main/resources/templates/edition/view.html
@@ -135,7 +135,7 @@
                 </a>
             </div>
             <th:block th:unless="${ec == null}">
-                <th:block th:replace="~{shared-edition/view :: tabs}"></th:block>
+                <th:block th:replace="~{shared-edition/tabs :: tabs}"></th:block>
             </th:block>
 
             <div class="alert alert-info mt-md-3" role="alert" th:unless="${#strings.isEmpty(message)}">
diff --git a/src/main/resources/templates/edition/view/participants.html b/src/main/resources/templates/edition/view/participants.html
index cf6e7569ce980e5832fe620065966348f4beea10..8d15040c082c07d8973987ed27b5cbd13ed2261e 100644
--- a/src/main/resources/templates/edition/view/participants.html
+++ b/src/main/resources/templates/edition/view/participants.html
@@ -66,9 +66,9 @@
                 </div>
 
                 <div class="surface p-0" th:with="headTAs = ${@roleDTOService.headTAs(edition)}">
-                    <h3 class="surface__header">Manager</h3>
+                    <h3 class="surface__header">Head TAs</h3>
 
-                    <div class="surface__content" th:if="${#lists.isEmpty(headTAs)}">There are no managers participating in this edition.</div>
+                    <div class="surface__content" th:if="${#lists.isEmpty(headTAs)}">There are no head TAs participating in this edition.</div>
 
                     <ul class="surface__content divided list" role="list" th:unless="${#lists.isEmpty(headTAs)}">
                         <li class="pbl-3 flex space-between" th:each="person : ${headTAs}">
@@ -88,9 +88,9 @@
                 </div>
 
                 <div class="surface p-0" th:with="assistants = ${@roleDTOService.assistants(edition)}">
-                    <h3 class="surface__header">Assistants</h3>
+                    <h3 class="surface__header">TAs</h3>
 
-                    <div class="surface__content" th:if="${#lists.isEmpty(assistants)}">There are no assistants participating in this edition.</div>
+                    <div class="surface__content" th:if="${#lists.isEmpty(assistants)}">There are no TAs participating in this edition.</div>
 
                     <ul class="surface__content divided list" role="list" th:unless="${#lists.isEmpty(assistants)}">
                         <li class="pbl-3 flex space-between" th:each="person : ${assistants}">
diff --git a/src/main/resources/templates/header.html b/src/main/resources/templates/header.html
index 8f69adde5b44da1d41f5a118fd58ec0e34873a5f..4df5f8874890fec24f714f74327c35509fa45d77 100644
--- a/src/main/resources/templates/header.html
+++ b/src/main/resources/templates/header.html
@@ -47,6 +47,9 @@
                 <li th:if="${@permissionService.canViewOwnFeedback()}">
                     <a th:href="@{/feedback/{id}(id=${#authenticatedP.id})}">Feedback</a>
                 </li>
+                <li th:if="${@permissionService.canViewRequests()}" class="flex vertical">
+                    <button data-dialog="language-overlay">Languages</button>
+                </li>
                 <li class="flex vertical">
                     <button data-dialog="theme-overlay">Appearance</button>
                 </li>
@@ -62,6 +65,7 @@
             <a th:href="@{/login}">Log in</a>
         </div>
 
+        <th:block layout:replace="~{home/language :: overlay}"></th:block>
         <th:block layout:replace="~{home/theme :: overlay}"></th:block>
     </header>
 </html>
diff --git a/src/main/resources/templates/history/index.html b/src/main/resources/templates/history/index.html
index f48490885ca8ad3e045a32634ebfed814705290d..cb2d5f8e3896633bab5bf113b4d04cb2706a7558 100644
--- a/src/main/resources/templates/history/index.html
+++ b/src/main/resources/templates/history/index.html
@@ -29,12 +29,12 @@
         <main layout:fragment="content">
             <h1 class="font-800 mb-5">My Requests</h1>
 
-            <th:block th:replace="request/list/filters :: filters (returnPath='/history')"></th:block>
+            <th:block th:replace="request/list/filters :: filters (returnPath='/history', multipleLabs=${true})"></th:block>
 
-            <div class="flex vertical">
+            <div class="flex vertical mb-5" style="overflow-x: auto">
                 <th:block th:replace="request/list/request-table :: request-table(showName=true, showOnlyRelevant=${false})"></th:block>
-                <th:block th:replace="pagination :: pagination (page=${requests}, size=3)"></th:block>
             </div>
+            <th:block th:replace="pagination :: pagination (page=${requests}, size=3)"></th:block>
         </main>
     </body>
 </html>
diff --git a/src/main/resources/templates/history/student.html b/src/main/resources/templates/history/student.html
index c6b4c3df21ed79935bd6f2bc94989870c5cdc84a..09d4ceb568c7f26014e7f998b4f9ce96118962e9 100644
--- a/src/main/resources/templates/history/student.html
+++ b/src/main/resources/templates/history/student.html
@@ -40,7 +40,7 @@
             </div>
 
             <th:block
-                th:replace="request/list/filters :: filters (returnPath=@{/history/course/{editionId}/student/{studentId}(editionId=${edition.id}, studentId=${student.id})})"></th:block>
+                th:replace="request/list/filters :: filters (returnPath=@{/history/course/{editionId}/student/{studentId}(editionId=${edition.id}, studentId=${student.id})}, multipleLabs=${true})"></th:block>
         </section>
 
         <section layout:fragment="outside-content">
diff --git a/src/main/resources/templates/home/dashboard.html b/src/main/resources/templates/home/dashboard.html
index cdfb3332561045a0b2776ff781962e070b3f43ca..514f57a554c13abeda51d0662b1e2391c8094c1b 100644
--- a/src/main/resources/templates/home/dashboard.html
+++ b/src/main/resources/templates/home/dashboard.html
@@ -294,14 +294,14 @@
                             <th:block th:each="role : ${visibleArchived}" th:with="edition = ${editions.get(role.edition.id)}">
                                 <li>
                                     <a
-                                        class="list"
+                                        class="link"
                                         th:href="@{/edition/{id}(id=${edition.id})}"
                                         th:text="|${edition.course.name} (${edition.name})|"></a>
                                     <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span>
                                 </li>
                             </th:block>
 
-                            <li th:each="shared : ${finishedSharedEditions}">
+                            <li th:each="shared : ${archivedSharedEditions}">
                                 <div>
                                     <a class="link" th:href="@{/shared-edition/{id}(id=${shared.id})}" th:text="|${shared.getName()}|"></a>
                                     <span class="chip">Shared edition</span>
diff --git a/src/main/resources/templates/home/language.html b/src/main/resources/templates/home/language.html
new file mode 100644
index 0000000000000000000000000000000000000000..2849ad84c997ca568ad323d64e7b2367193625ca
--- /dev/null
+++ b/src/main/resources/templates/home/language.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog layout:fragment="overlay" th:if="${@permissionService.canViewRequests()}" id="language-overlay" class="dialog" data-closable>
+        <form class="flex vertical p-7" id="profile-update-form">
+            <h2 class="font-500 underlined">Languages</h2>
+
+            <p>Select the language(s) in which you wish to handle requests.</p>
+
+            <div class="grid col-2 align-center gap-3" style="--col-1: minmax(0, 8rem)">
+                <label for="languages">Language(s)</label>
+                <select id="languages" name="language" class="textfield">
+                    <option value="ENGLISH_ONLY" th:selected="not ${#profile.language.name() == 'ENGLISH_ONLY'}">English only</option>
+                    <option value="ANY" th:selected="${#profile.language.name() == 'ANY'}">English and Dutch</option>
+                </select>
+            </div>
+
+            <div class="flex space-between">
+                <button type="button" class="button p-less" data-style="outlined" data-cancel>Close</button>
+                <button type="submit" class="button p-less">Update</button>
+            </div>
+        </form>
+
+        <script>
+            document.addEventListener("DOMContentLoaded", function () {
+                const form = document.getElementById("profile-update-form");
+                form.addEventListener("submit", function (event) {
+                    event.preventDefault();
+                    fetch("/profile/update", {
+                        method: "POST",
+                        headers: {
+                            "X-CSRF-TOKEN": csrfToken,
+                            "Content-Type": "application/json",
+                        },
+                        body: JSON.stringify(Object.fromEntries(new FormData(form).entries())),
+                    })
+                        .then(res => res.text())
+                        .then(() => window.location.reload())
+                        .catch(() => alert("Failed to update language preference"));
+                });
+            });
+        </script>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/lab/create/components/advanced/lab.html b/src/main/resources/templates/lab/create/components/advanced/lab.html
index b8423d6ebd821664b236d93ce09c002d78d26a9d..ba320cf2b576dc2479d45d4685abf06b4535b3f7 100644
--- a/src/main/resources/templates/lab/create/components/advanced/lab.html
+++ b/src/main/resources/templates/lab/create/components/advanced/lab.html
@@ -55,6 +55,10 @@
                 </div>
 
                 <div id="experimental-tab" hidden>
+                    <div>
+                        <input id="bilingual-toggle" type="checkbox" th:field="*{isBilingual}" />
+                        <label for="bilingual-toggle">Enable bilingual requests</label>
+                    </div>
                     <div>
                         <input id="experimental-toggle" class="custom-control-input" type="checkbox" th:field="*{enableExperimental}" />
                         <label class="custom-control-label" for="experimental-toggle">Enable experimental features</label>
diff --git a/src/main/resources/templates/lab/edit/components/advanced/lab.html b/src/main/resources/templates/lab/edit/components/advanced/lab.html
index 52bc87da406fde3b9e460a94b939548d5050a8e3..c011518a0c2882377fdbd84a72146071d3efebb3 100644
--- a/src/main/resources/templates/lab/edit/components/advanced/lab.html
+++ b/src/main/resources/templates/lab/edit/components/advanced/lab.html
@@ -55,6 +55,10 @@
                 </div>
 
                 <div id="experimental-tab" hidden>
+                    <div>
+                        <input id="bilingual-toggle" type="checkbox" name="isBilingual" th:checked="${lab.isBilingual}" />
+                        <label for="bilingual-toggle">Enable bilingual requests</label>
+                    </div>
                     <div>
                         <input id="experimental-toggle" type="checkbox" name="enableExperimental" th:checked="${lab.enableExperimental}" />
                         <label for="experimental-toggle">Enable experimental features</label>
diff --git a/src/main/resources/templates/lab/enqueue/lab.html b/src/main/resources/templates/lab/enqueue/lab.html
index 53f8f4f47e8e954bc16e3f9460ad80124530be98..36dc82fa493ec193aaca9c08cbfb2b6489682018 100644
--- a/src/main/resources/templates/lab/enqueue/lab.html
+++ b/src/main/resources/templates/lab/enqueue/lab.html
@@ -88,6 +88,16 @@
                         <a class="link" th:href="@{/edition/{id}/enrol(id=${notEnqueueAble.get(entry.key)})}">here</a>
                         to enrol.
                     </div>
+                    <div
+                        th:each="entry : ${assignments}"
+                        th:if="${needToJoinGroup.contains(entry.key)}"
+                        class="colour-error"
+                        style="display: none"
+                        th:id="|need-group-${entry.key}|">
+                        This assignment uses groups. Join a group
+                        <a class="link" th:href="@{/assignment/{id}/groups(id=${entry.key})}">here</a>
+                        first to enqueue.
+                    </div>
                     <div class="colour-error" th:if="${#fields.hasErrors('assignment')}" th:errors="*{assignment}">Assignment error</div>
                 </div>
 
@@ -144,6 +154,16 @@
                     <div class="colour-error" th:if="${#fields.hasErrors('onlineMode')}" th:errors="*{onlineMode}">Online Mode error</div>
                 </div>
 
+                <div id="language-div" th:if="${qSession.isBilingual}">
+                    <label for="input-language">Language</label>
+                    <div>
+                        <select id="input-language" data-select name="language">
+                            <option selected value="ANY">English</option>
+                            <option value="DUTCH_ONLY">Dutch</option>
+                        </select>
+                    </div>
+                </div>
+
                 <div id="question-div" th:classappend="${qSession.enableExperimental ? 'd-none' : ''}">
                     <label for="input-question" class="col-sm-2 control-label">Question</label>
                     <div class="flex vertical gap-1">
@@ -162,11 +182,13 @@
                 </div>
 
                 <div id="comment-div" th:classappend="${qSession.enableExperimental ? 'd-none' : ''}">
-                    <div id="image-holder" class="mb-3"></div>
-
                     <div class="flex vertical gap-1">
                         <label for="input-comment" id="labelComment">Help your TA find you!</label>
 
+                        <div id="image-holder" class="mt-1 mb-3" hidden>
+                            <img alt="Room map" />
+                        </div>
+
                         <textarea
                             maxlength="250"
                             th:classappend="${#fields.hasErrors('comment')} ? 'is-invalid'"
@@ -194,6 +216,16 @@
                         }
                     }
 
+                    function checkGroup(val) {
+                        const enqueue = document.getElementById("enqueue");
+                        const error = document.getElementById(`need-group-${val}`);
+                        enqueue.removeAttribute("disabled");
+                        if (error) {
+                            error.style.setProperty("display", "block");
+                            enqueue.setAttribute("disabled", "");
+                        }
+                    }
+
                     document.addEventListener("ComponentsLoaded", function () {
                         //<![CDATA[
                         const typesPerAssignment = /*[[${types}]]*/ { 1: ["bla"] };
@@ -210,6 +242,7 @@
 
                         assignmentSelect.addEventListener("change", function () {
                             checkEnrolled(this.value);
+                            checkGroup(this.value);
 
                             typeSelect.querySelectorAll("option").forEach(opt => opt.removeAttribute("selected"));
                             typeSelect.removeAttribute("disabled");
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 0000000000000000000000000000000000000000..cb70b3040ef1969908b6dc0ba482bc4f579f7145
--- /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 0000000000000000000000000000000000000000..73ca4b2f5d941c5fa498633f67dff0fe7e004828
--- /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 f86db548bc91b59184698dc046c63050ed4b4297..ddf28beec3e4b56175b4f343058819aea6388550 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>
@@ -92,9 +94,7 @@
                     </th:block>
                 </th:block>
 
-                <div class="grid auto-fit">
-                    <th:block layout:fragment="session-info"></th:block>
-                </div>
+                <th:block layout:fragment="session-info"></th:block>
 
                 <th:block layout:fragment="request-table"></th:block>
             </div>
diff --git a/src/main/resources/templates/lab/view/capacity.html b/src/main/resources/templates/lab/view/capacity.html
index 436b38ee451e1010627cbe4cab39db2432ea4b10..e70926bd6561bf9d537ec87ff074300e12ce19ff 100644
--- a/src/main/resources/templates/lab/view/capacity.html
+++ b/src/main/resources/templates/lab/view/capacity.html
@@ -25,9 +25,9 @@
     </head>
 
     <body>
-        <th:block layout:fragment="session-info">
+        <div class="grid auto-fit" layout:fragment="session-info">
             <th:block th:replace="lab/view/components/capacity-session-info :: capacity-lab-info"></th:block>
-        </th:block>
+        </div>
         <th:block layout:fragment="request-table">
             <th:block th:replace="lab/view/components/full-request-table :: full-request-table"></th:block>
         </th:block>
diff --git a/src/main/resources/templates/lab/view/components/full-request-table.html b/src/main/resources/templates/lab/view/components/full-request-table.html
index 000668dc58363a304139c2b588a8b8ef5c768443..4a7c4875aadb89460a34a8564974e196b5fc42f4 100644
--- a/src/main/resources/templates/lab/view/components/full-request-table.html
+++ b/src/main/resources/templates/lab/view/components/full-request-table.html
@@ -27,7 +27,13 @@
 
     <body>
         <div th:fragment="full-request-table" class="flex vertical gap-3">
-            <h3 class="font-500">Requests for this lab</h3>
+            <div class="flex space-between align-center">
+                <h3 class="font-500">Requests for this lab</h3>
+                <th:block th:if="${qSession instanceof T(nl.tudelft.queue.dto.view.LabViewDTO)}" th:with="assignments = ${allAssignments}">
+                    <th:block
+                        th:replace="~{request/list/filters :: filters(returnPath=@{/lab/{id}(id=${qSession.id})}, multipleLabs=${false})}"></th:block>
+                </th:block>
+            </div>
 
             <p th:if="${not @permissionService.canViewSessionRequests(qSession.id)}">
                 <th:block th:if="${current != null && current.eventInfo.status.isPending()}">You are waiting to be processed.</th:block>
@@ -82,15 +88,11 @@
                     </td>
                     <td>
                         <th:block
-                            th:if="${@permissionService.canGiveFeedback(request.id) and request instanceof T(nl.tudelft.queue.model.LabRequest)}">
-                            <span th:if="${request.eventInfo.status.isHandled() and !request.feedbacks.isEmpty()}">
-                                You have given feedback to your teaching assistant.
-                            </span>
+                            th:if="${@permissionService.canGiveFeedback(request.id) and request instanceof T(nl.tudelft.queue.dto.view.requests.LabRequestViewDTO)}">
+                            <span th:if="${request.eventInfo.status.isHandled() and !request.feedbacks.isEmpty()}">Feedback given</span>
 
                             <th:block th:if="${request.eventInfo.status.isHandled() and request.feedbacks.isEmpty()}">
-                                <a class="link" style="color: var(--colour)" th:href="@{/request/{id}(id=${request.id})}">
-                                    Click to add feedback for the Teaching assistant
-                                </a>
+                                <a class="link" style="color: var(--colour)" th:href="@{/request/{id}(id=${request.id})}">Click to give feedback</a>
                             </th:block>
                         </th:block>
                     </td>
diff --git a/src/main/resources/templates/lab/view/components/lab-info.html b/src/main/resources/templates/lab/view/components/lab-info.html
index 1a07ea896a9160b56d4b220ddb075f8e11b37cec..e5e51a2a758720f0897a53122c5a451ef9bae60f 100644
--- a/src/main/resources/templates/lab/view/components/lab-info.html
+++ b/src/main/resources/templates/lab/view/components/lab-info.html
@@ -54,7 +54,11 @@
                         <dd th:if="${#lists.isEmpty(modules)}">This lab does not have any modules configured</dd>
                         <dd th:unless="${#lists.isEmpty(modules)}">
                             <ul class="list">
-                                <li th:each="m : ${modules}" th:text="${m.name}"></li>
+                                <li th:if="${qSession.session.editions.size() == 1}" th:each="m : ${modules}" th:text="${m.name}"></li>
+                                <li
+                                    th:if="${qSession.session.editions.size() > 1}"
+                                    th:each="m : ${modules}"
+                                    th:text="|${@editionCacheManager.getRequired(m.edition.id).course.name} - ${m.name}|"></li>
                             </ul>
                         </dd>
 
@@ -79,6 +83,11 @@
                                 </ul>
                             </dd>
                         </th:block>
+
+                        <th:block th:if="${qSession.isBilingual}">
+                            <dt class="fw-500 mt-3">Languages</dt>
+                            <dd>Requests can be done in English or Dutch.</dd>
+                        </th:block>
                     </dl>
                 </div>
             </div>
@@ -147,11 +156,6 @@
                     </dd>
                 </div>
             </div>
-
-            <div th:if="${!#strings.isEmpty(qSession.extraInfo)}">
-                <h3>Extra information for this lab</h3>
-                <div th:utext="${qSession.extraInfo}"></div>
-            </div>
         </th:block>
     </body>
 </html>
diff --git a/src/main/resources/templates/lab/view/lab.html b/src/main/resources/templates/lab/view/lab.html
index db7c6ecc37bb9816f6db571990e2380e15960ff8..1589dce52596a6e06d5684b5209ee4fd0bbefd7e 100644
--- a/src/main/resources/templates/lab/view/lab.html
+++ b/src/main/resources/templates/lab/view/lab.html
@@ -26,8 +26,52 @@
 
     <body>
         <th:block layout:fragment="session-info">
-            <th:block th:replace="lab/view/components/lab-info :: lab-info"></th:block>
-            <th:block layout:fragment="additional-lab-info"></th:block>
+            <div class="grid auto-fit">
+                <th:block th:replace="lab/view/components/lab-info :: lab-info"></th:block>
+                <th:block layout:fragment="additional-lab-info"></th:block>
+            </div>
+
+            <div class="surface p-0" th:if="${!#strings.isEmpty(qSession.extraInfo)}">
+                <h3 class="surface__header">Extra information for this lab</h3>
+                <div class="surface__content" th:utext="${qSession.extraInfo}"></div>
+            </div>
+
+            <dialog th:if="${param.requestFinished != null}" id="feedback-reminder" class="dialog">
+                <div class="flex vertical p-7">
+                    <h3 class="font-500 underlined">Give feedback</h3>
+                    <p>Do you want to give feedback to your TA?</p>
+                    <div>
+                        <input type="checkbox" id="no-feedback-reminder" />
+                        <label for="no-feedback-reminder">Do not show this again</label>
+                        <p class="font-200">(you can always give feedback by going to your 'Request history' in the top right)</p>
+                    </div>
+                    <div class="flex space-between" id="feedback-buttons">
+                        <button data-style="outlined" class="button p-less" data-cancel>No feedback</button>
+                        <a th:href="@{/request/{id}(id=${param.requestFinished})}" class="button p-less">Give feedback</a>
+                    </div>
+                </div>
+            </dialog>
+
+            <script>
+                const url = new URL(window.location.href);
+                if (url.searchParams.has("requestFinished") && localStorage.getItem("do-not-show-feedback-reminders") !== "true") {
+                    document.getElementById("feedback-reminder").showModal();
+                    document.activeElement.blur();
+                }
+                document.addEventListener("DOMContentLoaded", function () {
+                    document
+                        .getElementById("feedback-buttons")
+                        .querySelectorAll(".button")
+                        .forEach(b =>
+                            b.addEventListener("click", function () {
+                                const doNotShowAgain = document.getElementById("no-feedback-reminder").checked;
+                                if (doNotShowAgain) {
+                                    localStorage.setItem("do-not-show-feedback-reminders", "true");
+                                }
+                            })
+                        );
+                });
+            </script>
         </th:block>
     </body>
 </html>
diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html
index 95cfcfeafa2cb7da5e16a2f1e35ca83054593544..45a208f5a7929169661e8a8b533a0cb46106be46 100644
--- a/src/main/resources/templates/layout.html
+++ b/src/main/resources/templates/layout.html
@@ -36,11 +36,12 @@
         <title th:if="${@thymeleafConfig.isTheDay()}">Stack</title>
         <title>Queue</title>
 
-        <link rel="stylesheet" href="/webjars/chihuahui/1.0.1/main.css" />
+        <link rel="stylesheet" href="/webjars/chihuahui/main.css" />
         <link rel="stylesheet" href="/webjars/font-awesome/css/all.css" />
 
-        <script type="module" src="/webjars/chihuahui/1.0.1/components.js"></script>
-        <script src="/webjars/chihuahui/1.0.1/theme.js"></script>
+        <script type="module" src="/webjars/chihuahui/components.js"></script>
+        <script src="/webjars/chihuahui/theme.js"></script>
+        <script src="/webjars/chihuahui/toast.js"></script>
         <script src="/webjars/jquery/jquery.min.js"></script>
         <script src="/webjars/jquery-cookie/jquery.cookie.js"></script>
 
@@ -50,9 +51,10 @@
         <script src="/js/global.js"></script>
 
         <script type="application/javascript">
+            const csrfToken = $("meta[name='_csrf']").attr("content");
             $.ajaxSetup({
                 headers: {
-                    "X-CSRF-TOKEN": $("meta[name='_csrf']").attr("content"),
+                    "X-CSRF-TOKEN": csrfToken,
                 },
             });
         </script>
diff --git a/src/main/resources/templates/module/groups.html b/src/main/resources/templates/module/groups.html
new file mode 100644
index 0000000000000000000000000000000000000000..512dee1dc1c8eeaaa2f82dae6262a93befa02f8c
--- /dev/null
+++ b/src/main/resources/templates/module/groups.html
@@ -0,0 +1,50 @@
+<!--
+
+    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}">
+    <!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+    <!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleDetailsDTO"-->
+
+    <body>
+        <section class="flex vertical gap-3" layout:fragment="subcontent">
+            <h3 class="font-500" th:text="|${module.name} - Join a group|"></h3>
+
+            <table class="table" data-style="surface">
+                <tr class="table__header">
+                    <td>Group</td>
+                    <td>Capacity</td>
+                    <td></td>
+                </tr>
+                <tr th:each="group : ${groups}" th:if="${group.memberUsernames.size() < group.capacity}">
+                    <td th:text="${group.name}"></td>
+                    <td>
+                        <span class="chip" th:text="|${group.memberUsernames.size()} / ${group.capacity}|"></span>
+                    </td>
+                    <td class="flex justify-end">
+                        <form th:action="@{/group/{id}/join(id=${group.id})}" th:method="post">
+                            <button type="submit" class="button p-min">Join</button>
+                        </form>
+                    </td>
+                </tr>
+            </table>
+        </section>
+    </body>
+</html>
diff --git a/src/main/resources/templates/request/list.html b/src/main/resources/templates/request/list.html
index 3f3564bc57ca391c928b6460fd6bc9e2822aa4f1..23fbf1935f8f4de0c60c8e37a3e0f967c99779b0 100644
--- a/src/main/resources/templates/request/list.html
+++ b/src/main/resources/templates/request/list.html
@@ -70,7 +70,7 @@
         </div>
 
         <th:block
-            th:replace="request/list/filters :: filters (returnPath=${#request.requestURI.matches('.*/history.*') ? '/requests/history' : '/requests'})"></th:block>
+            th:replace="request/list/filters :: filters (returnPath=${#request.requestURI.matches('.*/history.*') ? '/requests/history' : '/requests'}, multipleLabs=${true})"></th:block>
 
         <div class="flex gap-3 wrap mb-5" th:unless="${#request.requestURI.matches('.*/history.*')}">
             <form th:action="@{/requests/next/{labId}(labId = ${lab.id})}" method="get" th:each="lab : ${labs}">
diff --git a/src/main/resources/templates/request/list/filters.html b/src/main/resources/templates/request/list/filters.html
index 4ff1e93eb21edb7a5da94e016f95ba162d896681..828ed2a43c96f57bdddcfd10b7e57a4b4dde53a8 100644
--- a/src/main/resources/templates/request/list/filters.html
+++ b/src/main/resources/templates/request/list/filters.html
@@ -30,7 +30,7 @@
     <!--@thymesVar id="returnPath" type="java.lang.String"-->
 
     <body>
-        <div class="mb-5" th:fragment="filters(returnPath)">
+        <div class="mb-5" th:fragment="filters(returnPath, multipleLabs)">
             <div>
                 <button type="button" class="button" data-style="outlined" data-dialog="filter-modal">
                     <span class="fa fa-filter"></span>
@@ -46,12 +46,14 @@
                     <div id="filter-form" class="grid col-2 align-center" style="--col-1: minmax(0, 8rem)">
                         <input type="hidden" name="return-path" th:value="${returnPath}" />
 
-                        <label class="form-control-label" for="lab-select">Lab</label>
-                        <select multiple class="select" data-select id="lab-select" th:field="*{labs}">
-                            <th:block th:each="lab : ${labs}">
-                                <option th:value="${lab.id}" th:selected="${filter.labs.contains(lab.id)}" th:text="${lab.readableName}"></option>
-                            </th:block>
-                        </select>
+                        <th:block th:if="${multipleLabs}">
+                            <label class="form-control-label" for="lab-select">Lab</label>
+                            <select multiple class="select" data-select id="lab-select" th:field="*{labs}">
+                                <th:block th:each="lab : ${labs}">
+                                    <option th:value="${lab.id}" th:selected="${filter.labs.contains(lab.id)}" th:text="${lab.readableName}"></option>
+                                </th:block>
+                            </select>
+                        </th:block>
 
                         <label class="form-control-label" for="assignment-select">Assignment</label>
                         <select multiple class="select" data-select id="assignment-select" th:field="*{assignments}">
diff --git a/src/main/resources/templates/request/list/request-table.html b/src/main/resources/templates/request/list/request-table.html
index 6f9f9071418667ab446838f74d7721c7790a6bcf..8b6b64d25e0128903a4f925b00b5f6b2f17e3243 100644
--- a/src/main/resources/templates/request/list/request-table.html
+++ b/src/main/resources/templates/request/list/request-table.html
@@ -24,16 +24,18 @@
     <body>
         <th:block th:fragment="request-table(showName, showOnlyRelevant)">
             <table id="request-table" class="table">
-                <tr class="table__header">
-                    <th>Status</th>
-                    <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Type</th>
-                    <th th:if="${showName}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Requested by</th>
-                    <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Room</th>
-                    <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Assignment</th>
-                    <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Course</th>
-                    <th th:unless="${showOnlyRelevant}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Assigned</th>
-                    <th th:unless="${showOnlyRelevant}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Handled</th>
-                </tr>
+                <thead>
+                    <tr class="table__header">
+                        <th>Status</th>
+                        <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Type</th>
+                        <th th:if="${showName}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Requested by</th>
+                        <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Room</th>
+                        <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Assignment</th>
+                        <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Course</th>
+                        <th th:unless="${showOnlyRelevant}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Assigned</th>
+                        <th th:unless="${showOnlyRelevant}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Handled</th>
+                    </tr>
+                </thead>
                 <tbody th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-down'">
                     <tr th:if="${requests.isEmpty()}" id="no-requests-info">
                         <td colspan="8">There aren't any requests.</td>
@@ -123,43 +125,42 @@
 
             <!-- Changes made to this template should be made accordingly to the template in request/list/request-table.html -->
             <script id="request-entry-template" type="text/x-handlebars-template">
-                <tr class="text-white bg-pending" id="request-{{id}}" th:with="base = @{/request}">
-                    <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'">
-                        <span class="badge badge-pill bg-danger" id="status-{{id}}">
-                            NEW
+                <tr class="request" data-status="pending" id="request-{{id}}" data-request="{{id}}" role="link" tabindex="0">
+                    <td class="fit-content">
+                        <span class="chip single-line" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'" id="status-{{id}}">
+                            New
                         </span>
                     </td>
                     <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'">
-                        <a th:href="${base + '/{{id}}'}" class="text-white">
-                            {{requestTypeDisplayName}}
-                        </a>
+                        {{requestTypeDisplayName}}
                     </td>
                     {{#if roomName}}
                         {{#if buildingName}}
                             <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'">
-                                <a th:href="${base + '/{{id}}'}" class="text-white">
+                                <span class="single-line">
                                     {{buildingName}} - {{roomName}}
-                                </a>
+                                </span>
                             </td>
                         {{/if}}
                     {{else if onlineMode}}
                         <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'">
-                            <a th:href="${base + '/{{id}}'}" class="text-white">
+                            <span class="single-line">
                                 @Online - {{onlineModeDisplayName}}
-                            </a>
+                            </span>
                         </td>
                     {{/if}}
                     <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'">
-                        <a th:href="${base + '/{{id}}'}" class="text-white">
-                            {{assignmentName}} ({{moduleName}})
-                        </a>
-                        <br />
-                        <small>
-                            Right now
-                        </small>
+                        <div class="flex vertical gap-0">
+                            <span class="single-line">
+                                {{assignmentName}} ({{moduleName}})
+                            </span>
+                            <span class="font-100">
+                                Right now
+                            </span>
+                        </div>
                     </td>
                     <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'">
-                        <span class="d-inline-block">
+                        <span class="single-line">
                             {{organizationName}}
                         </span>
                     </td>
diff --git a/src/main/resources/templates/request/view/components/feedback.html b/src/main/resources/templates/request/view/components/feedback.html
index 16433f26a98430521830db94d2a4a63c0d5d0a65..9c5407e0c7f6ce4c5a779de782bb50470ac0d607 100644
--- a/src/main/resources/templates/request/view/components/feedback.html
+++ b/src/main/resources/templates/request/view/components/feedback.html
@@ -119,7 +119,9 @@
                             th:placeholder="|Leave feedback about ${assistant.displayName}.|"></textarea>
                         <div>
                             <p class="font-200">
-                                Your feedback is anonymous: The TA will only be able to see the feedback and score you give, not your name.
+                                Your feedback is
+                                <strong class="fw-500">anonymous</strong>
+                                : The TA will only be able to see the feedback and score you give, not your name.
                             </p>
                         </div>
                         <div>
diff --git a/src/main/resources/templates/request/view/components/history.html b/src/main/resources/templates/request/view/components/history.html
index 1495d1d33b66c94bc9e32f9be547350e3849649b..d759d6507bc228b843d7e0bd14183d1ad68d694f 100644
--- a/src/main/resources/templates/request/view/components/history.html
+++ b/src/main/resources/templates/request/view/components/history.html
@@ -83,8 +83,6 @@
                     </div>
                 </li>
             </ul>
-
-            <div id="image-holder"></div>
         </div>
     </body>
 </html>
diff --git a/src/main/resources/templates/request/view/components/lab-request-info.html b/src/main/resources/templates/request/view/components/lab-request-info.html
index 3019d9c9bd1aa7c100cc6fe1f86e7bf14c4a39f4..49591c9420a878c07cf37be07dcbbe316d8d205d 100644
--- a/src/main/resources/templates/request/view/components/lab-request-info.html
+++ b/src/main/resources/templates/request/view/components/lab-request-info.html
@@ -58,6 +58,14 @@
                         <dt class="fw-500 mt-3">Type</dt>
                         <dd th:text="${request.requestType.displayName()}"></dd>
 
+                        <th:block th:if="${request.qSession.isBilingual}">
+                            <dt class="fw-500 mt-3">Language</dt>
+                            <th:block th:switch="${request.language.name()}">
+                                <dd th:case="'ANY'">English or Dutch</dd>
+                                <dd th:case="'DUTCH_ONLY'">Dutch</dd>
+                            </th:block>
+                        </th:block>
+
                         <th:block th:unless="${#strings.isEmpty(request.question)}">
                             <dt class="fw-500 mt-3">Question</dt>
                             <dd th:text="${request.question}"></dd>
@@ -124,6 +132,15 @@
                 </div>
             </div>
 
+            <div id="image-holder" class="mt-5" hidden>
+                <div class="surface p-0">
+                    <h3 class="surface__header">Room map</h3>
+                    <div class="surface__content">
+                        <img alt="Room map" />
+                    </div>
+                </div>
+            </div>
+
             <div class="mt-5" th:if="${@permissionService.canViewRequestAssistantReason(request.id)}">
                 <h3 class="font-500 mb-3">Previous</h3>
 
diff --git a/src/main/resources/templates/shared-edition/tabs.html b/src/main/resources/templates/shared-edition/tabs.html
new file mode 100644
index 0000000000000000000000000000000000000000..3c8ca371e33bf9374b531d767dc8563d6db1611f
--- /dev/null
+++ b/src/main/resources/templates/shared-edition/tabs.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{container}">
+    <body>
+        <th:block th:fragment="tabs">
+            <div class="tabs mb-5" role="tablist">
+                <a id="sessions-tab" role="tab" th:href="@{/shared-edition/{id}?page=sessions(id=${ec.id})}" aria-selected="false">
+                    <span class="fa-solid fa-calendar"></span>
+                    Sessions
+                </a>
+                <a id="editions-tab" role="tab" th:href="@{/shared-edition/{id}?page=editions(id=${ec.id})}" aria-selected="false">
+                    <span class="fa-solid fa-graduation-cap"></span>
+                    Editions
+                </a>
+                <a id="staff-tab" role="tab" th:href="@{/shared-edition/{id}?page=staff(id=${ec.id})}" aria-selected="false">
+                    <span class="fa-solid fa-user"></span>
+                    Staff
+                </a>
+            </div>
+        </th:block>
+    </body>
+</html>
diff --git a/src/main/resources/templates/shared-edition/view.html b/src/main/resources/templates/shared-edition/view.html
index b23a6616e7be9eee65d3d87c4e351a65369968b9..13582fe8b5bd6d804bd8631bf323087c04293458 100644
--- a/src/main/resources/templates/shared-edition/view.html
+++ b/src/main/resources/templates/shared-edition/view.html
@@ -24,26 +24,26 @@
             </div>
             <th:block th:replace="~{shared-edition/view/create-session-dialog :: overlay}"></th:block>
 
-            <div th:fragment="tabs" class="tabs mb-5" role="tablist">
-                <button id="sessions-tab" role="tab" aria-controls="sessions" aria-selected="true">
+            <div class="tabs mb-5" role="tablist" th:with="page = ${param.page}">
+                <button id="sessions-tab" role="tab" aria-controls="sessions" th:aria-selected="${page == null} or ${page?.toString() == 'sessions'}">
                     <span class="fa-solid fa-calendar"></span>
                     Sessions
                 </button>
-                <button id="editions-tab" role="tab" aria-controls="editions" aria-selected="false">
+                <button id="editions-tab" role="tab" aria-controls="editions" th:aria-selected="${page?.toString() == 'editions'}">
                     <span class="fa-solid fa-graduation-cap"></span>
                     Editions
                 </button>
-                <button id="staff-tab" role="tab" aria-controls="staff" aria-selected="false">
+                <button id="staff-tab" role="tab" aria-controls="staff" th:aria-selected="${page?.toString() == 'staff'}">
                     <span class="fa-solid fa-user"></span>
                     Staff
                 </button>
             </div>
 
-            <div id="sessions">
+            <div id="sessions" th:hidden="${param.page != null} and ${param.page.toString() != 'sessions'}">
                 <th:block th:replace="~{shared-edition/view/session-list :: tab}"></th:block>
             </div>
 
-            <div id="editions" hidden>
+            <div id="editions" th:hidden="${param.page?.toString() != 'editions'}">
                 <div class="flex vertical">
                     <table class="table" data-style="surface">
                         <tr class="table__header">
@@ -64,7 +64,7 @@
                 </div>
             </div>
 
-            <div id="staff" hidden>
+            <div id="staff" th:hidden="${param.page?.toString != 'staff'}">
                 <div class="flex vertical">
                     <table class="table" data-style="surface">
                         <tr class="table__header">
diff --git a/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java b/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java
index 91bc66f6c233f79dea3ed4f60b9ce072ae910f65..7dac8658472846052ecf43095616fcd81cf520de 100644
--- a/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java
+++ b/src/test/java/nl/tudelft/queue/controller/LabControllerTest.java
@@ -31,6 +31,7 @@ import java.util.Set;
 import javax.transaction.Transactional;
 
 import nl.tudelft.labracore.api.SessionControllerApi;
+import nl.tudelft.labracore.api.StudentGroupControllerApi;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.queue.dto.create.labs.CapacitySessionCreateDTO;
 import nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO;
@@ -72,6 +73,7 @@ import org.springframework.security.test.context.support.WithUserDetails;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import test.TestDatabaseLoader;
 import test.test.TestQueueApplication;
@@ -114,6 +116,12 @@ class LabControllerTest {
 	@Autowired
 	private SessionControllerApi sApi;
 
+	@Autowired
+	private LabRepository lr;
+
+	@Autowired
+	private StudentGroupControllerApi sgApi;
+
 	@Captor
 	private ArgumentCaptor<QueueSession<LabRequest>> queueSessionArgumentCaptor;
 
@@ -148,6 +156,8 @@ class LabControllerTest {
 		when(sApi.addSharedSession(any())).thenReturn(Mono.just(667L));
 		when(sApi.patchSession(any(), any())).thenReturn(Mono.just(668L));
 
+		when(sgApi.getAllGroupsInModule(anyLong())).thenReturn(Flux.empty());
+
 		teacher1 = db.getTeachers()[1];
 		student5 = db.getStudents()[5];
 
@@ -717,6 +727,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 {
diff --git a/src/test/java/nl/tudelft/queue/controller/ProfileControllerTest.java b/src/test/java/nl/tudelft/queue/controller/ProfileControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..bde5f0513b58bd617d434f3ac4e60afebaab2e19
--- /dev/null
+++ b/src/test/java/nl/tudelft/queue/controller/ProfileControllerTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.controller;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import javax.transaction.Transactional;
+
+import nl.tudelft.queue.model.enums.Language;
+import nl.tudelft.queue.repository.ProfileRepository;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.MockMvc;
+
+import test.labracore.PersonApiMocker;
+import test.test.TestQueueApplication;
+
+@Transactional
+@AutoConfigureMockMvc
+@SpringBootTest(classes = TestQueueApplication.class)
+public class ProfileControllerTest {
+
+	@Autowired
+	private MockMvc mvc;
+	@Autowired
+	private ProfileRepository profileRepository;
+	@Autowired
+	private PersonApiMocker pApiMocker;
+
+	private ProfileController profileController;
+
+	@BeforeEach
+	void setUp() {
+		profileController = new ProfileController(profileRepository);
+	}
+
+	@Test
+	@WithUserDetails("teacher1")
+	void updateProfile() throws Exception {
+		var person = pApiMocker.getByUsername("teacher1").get();
+		mvc.perform(post("/profile/update").with(csrf())
+				.header("Content-Type", "application/json")
+				.content("{\"language\": \"ENGLISH_ONLY\"}"))
+				.andExpect(status().isOk());
+		assertThat(profileRepository.findById(person.getId()).get().getLanguage())
+				.isEqualTo(Language.ENGLISH_ONLY);
+	}
+
+}