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); + } + +}