diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bd57e816d8f17f31c92e8bbb37099fb257712d4..1aad7567d23f97a666f77d2f80a091e1976b9b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Changed + - Course editions not created in Queue will now not be displayed until the teacher 'unhides' them. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) ### Fixed diff --git a/src/main/java/nl/tudelft/queue/controller/EditionController.java b/src/main/java/nl/tudelft/queue/controller/EditionController.java index f370d8bbd8d785afc17382fe0d1912a023b35d2b..226dce53e8f4b53c75c6ab4379126a2bd790d8b5 100644 --- a/src/main/java/nl/tudelft/queue/controller/EditionController.java +++ b/src/main/java/nl/tudelft/queue/controller/EditionController.java @@ -18,7 +18,6 @@ package nl.tudelft.queue.controller; import static java.time.LocalDateTime.now; -import static nl.tudelft.labracore.lib.LabracoreApiUtil.fromPageable; import static nl.tudelft.queue.PageUtil.toPage; import java.io.IOException; @@ -29,13 +28,16 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import javax.servlet.http.HttpServletResponse; +import javax.transaction.Transactional; import javax.validation.Valid; import nl.tudelft.labracore.api.*; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; +import nl.tudelft.labracore.lib.security.user.DefaultRole; import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.librador.dto.view.View; +import nl.tudelft.queue.PageUtil; import nl.tudelft.queue.cache.*; import nl.tudelft.queue.csv.EmptyCsvException; import nl.tudelft.queue.csv.InvalidCsvException; @@ -43,17 +45,19 @@ import nl.tudelft.queue.dto.create.CourseRequestCreateDTO; import nl.tudelft.queue.dto.create.QueueEditionCreateDTO; import nl.tudelft.queue.dto.create.QueueRoleCreateDTO; import nl.tudelft.queue.dto.util.EditionFilterDTO; +import nl.tudelft.queue.dto.view.QueueEditionDetailsDTO; import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO; import nl.tudelft.queue.model.LabRequest; +import nl.tudelft.queue.model.QueueEdition; import nl.tudelft.queue.model.enums.QueueSessionType; import nl.tudelft.queue.model.labs.Lab; +import nl.tudelft.queue.repository.QueueEditionRepository; import nl.tudelft.queue.repository.QueueSessionRepository; import nl.tudelft.queue.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.core.io.Resource; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; @@ -138,6 +142,9 @@ public class EditionController { @Autowired private QuestionService qs; + @Autowired + private QueueEditionRepository qer; + @Autowired @Lazy private LabService ls; @@ -159,16 +166,18 @@ public class EditionController { PersonDetailsDTO pd = Objects.requireNonNull(pApi.getPersonById(person.getId()).block()); var filter = es.getFilter("/editions"); - var editions = eApi - .getEditionsPageActiveOrTaughtBy(person.getId(), fromPageable(pageable), filter.getPrograms(), - filter.getNameSearch()) + List<EditionDetailsDTO> lcEditions = eApi.getAllEditionsActiveOrTaughtBy(person.getId()).collectList() .block(); + eCache.register(lcEditions); + erCache.getAndIgnoreMissing(lcEditions.stream().map(EditionDetailsDTO::getId)); - eCache.register(editions.getContent()); - erCache.getAndIgnoreMissing(editions.getContent().stream().map(EditionDetailsDTO::getId)); + var editions = es.queueEditionDTO(lcEditions, QueueEditionDetailsDTO.class); + if (person.getDefaultRole() != DefaultRole.ADMIN) { + editions = editions.stream().filter(e -> !e.getHidden()).toList(); + } + var page = PageUtil.toPage(pageable, editions); - model.addAttribute("editions", - new PageImpl<>(editions.getContent(), pageable, editions.getTotalElements())); + model.addAttribute("editions", page); model.addAttribute("programs", cCache.getAll() .stream().map(CourseDetailsDTO::getProgram).distinct() .sorted(Comparator.comparing(ProgramSummaryDTO::getName)) @@ -257,7 +266,8 @@ public class EditionController { */ @GetMapping("/edition/{editionId}") public String getEditionView(@PathVariable Long editionId, Model model) { - EditionDetailsDTO edition = eCache.getRequired(editionId); + QueueEditionDetailsDTO edition = es.queueEditionDTO(eCache.getRequired(editionId), + QueueEditionDetailsDTO.class); model.addAttribute("edition", edition); model.addAttribute("assignments", @@ -288,7 +298,7 @@ public class EditionController { students = es.studentsMatchingFilter(students, studentSearch); } - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("assignments", mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId)) .stream() @@ -322,7 +332,7 @@ public class EditionController { sgCache.getAndIgnoreMissing(modules.stream() .flatMap(m -> m.getGroups().stream().map(StudentGroupSmallSummaryDTO::getId))); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("modules", modules); model.addAttribute("assignments", modules.stream().flatMap(m -> m.getAssignments().stream()).toList()); @@ -363,7 +373,7 @@ public class EditionController { // Sort all labs labs = es.sortLabs(labs); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("labs", labs); model.addAttribute("allLabTypes", QueueSessionType.values()); model.addAttribute("queueSessionTypes", queueSessionTypes); @@ -389,7 +399,7 @@ public class EditionController { public String getEditionQuestions(@PathVariable Long editionId, Model model) { EditionDetailsDTO edition = eCache.getRequired(editionId); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("assignments", mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId)) .stream() @@ -411,7 +421,7 @@ public class EditionController { public String getAddParticipantPage(@PathVariable Long editionId, Model model) { EditionDetailsDTO edition = eCache.getRequired(editionId); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("assignments", mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId)) .stream() @@ -439,7 +449,7 @@ public class EditionController { if (dto.hasErrors()) { EditionDetailsDTO edition = eCache.getRequired(editionId); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("assignments", mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId)) .stream() @@ -505,7 +515,7 @@ public class EditionController { Model model) { EditionDetailsDTO edition = eCache.getRequired(editionId); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("assignments", mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId)) .stream() @@ -558,7 +568,7 @@ public class EditionController { public String getParticipantLeavePage(@PathVariable Long editionId, Model model) { EditionDetailsDTO edition = eCache.getRequired(editionId); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("assignments", mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId)) .stream() @@ -600,7 +610,7 @@ public class EditionController { var sessions = sCache.getAndHandleAll(edition.getSessions().stream().map(SessionSummaryDTO::getId), ls.deleteSessionsByIds()); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("assignments", mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId)) .stream() @@ -672,6 +682,7 @@ public class EditionController { EditionCreateDTO edition = create.apply(); Long id = eApi.addEdition(edition).block(); + qer.save(QueueEdition.builder().id(id).hidden(false).build()); return "redirect:/edition/" + id; } @@ -718,7 +729,7 @@ public class EditionController { Model model) { EditionDetailsDTO edition = eCache.getRequired(editionId); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("assignments", mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId)) .stream() @@ -764,7 +775,7 @@ public class EditionController { Model model) { EditionDetailsDTO edition = eCache.getRequired(editionId); - model.addAttribute("edition", edition); + model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class)); model.addAttribute("assignments", mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId)) .stream() @@ -842,4 +853,20 @@ public class EditionController { QueueSessionSummaryDTO.class); } + /** + * Toggles the visibility of an edition in Queue. + * + * @param editionId The id of the edition + * @return A redirect to the edition page + */ + @Transactional + @PostMapping("/edition/{editionId}/visibility") + @PreAuthorize("@permissionService.canManageEdition(#editionId)") + public String toggleVisibility(@PathVariable Long editionId) { + QueueEdition qEdition = es.getOrCreateQueueEdition(editionId); + qEdition.setHidden(!qEdition.getHidden()); + qer.save(qEdition); + return "redirect:/edition/{editionId}"; + } + } diff --git a/src/main/java/nl/tudelft/queue/controller/HomeController.java b/src/main/java/nl/tudelft/queue/controller/HomeController.java index 20766ace181932bc2e824bee04a908a51916fc96..2383940db42a7af9440d7f056183132a85e3b9c6 100644 --- a/src/main/java/nl/tudelft/queue/controller/HomeController.java +++ b/src/main/java/nl/tudelft/queue/controller/HomeController.java @@ -23,7 +23,6 @@ import static nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO.TypeEnum.TEACHE import java.time.LocalDateTime; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; import nl.tudelft.labracore.api.EditionControllerApi; @@ -38,6 +37,7 @@ import nl.tudelft.queue.cache.EditionCollectionCacheManager; import nl.tudelft.queue.cache.PersonCacheManager; import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.dto.view.FeedbackViewDTO; +import nl.tudelft.queue.dto.view.QueueEditionDetailsDTO; import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO; import nl.tudelft.queue.model.Feedback; import nl.tudelft.queue.repository.FeedbackRepository; @@ -173,7 +173,8 @@ public class HomeController { var editions = eCache.getAndIgnoreMissing(roles.stream().map(r -> r.getEdition().getId())) .stream() - .collect(Collectors.toMap(EditionDetailsDTO::getId, Function.identity())); + .collect(Collectors.toMap(EditionDetailsDTO::getId, + e -> es.queueEditionDTO(e, QueueEditionDetailsDTO.class))); Set<SessionDetailsDTO> sessions = new HashSet<>(sCache.getAndHandleAll(editions.values().stream() .flatMap(e -> e.getSessions().stream()) diff --git a/src/main/java/nl/tudelft/queue/controller/LabController.java b/src/main/java/nl/tudelft/queue/controller/LabController.java index 773c15859e3dacef9e774fd926798cc6f54b6895..b38f40e375b065a300eaf4736f8070b2c53fd013 100644 --- a/src/main/java/nl/tudelft/queue/controller/LabController.java +++ b/src/main/java/nl/tudelft/queue/controller/LabController.java @@ -87,6 +87,9 @@ public class LabController { @Autowired private LabService ls; + @Autowired + private EditionService es; + @Autowired private RequestTableService rts; diff --git a/src/main/java/nl/tudelft/queue/dto/view/QueueEditionDetailsDTO.java b/src/main/java/nl/tudelft/queue/dto/view/QueueEditionDetailsDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..05cea4e6b4cd4649f8f780b4e9d124af3c1bdaa5 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/dto/view/QueueEditionDetailsDTO.java @@ -0,0 +1,35 @@ +/* + * 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.view; + +import javax.validation.constraints.NotNull; + +import lombok.*; +import nl.tudelft.labracore.api.dto.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class QueueEditionDetailsDTO extends EditionDetailsDTO implements QueueEditionViewDTO { + + @NotNull + private Boolean hidden; + +} diff --git a/src/main/java/nl/tudelft/queue/dto/view/QueueEditionSummaryDTO.java b/src/main/java/nl/tudelft/queue/dto/view/QueueEditionSummaryDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..3f6d65b0a6ae1c69ce36e7a4e72d80404ed7dffe --- /dev/null +++ b/src/main/java/nl/tudelft/queue/dto/view/QueueEditionSummaryDTO.java @@ -0,0 +1,35 @@ +/* + * 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.view; + +import javax.validation.constraints.NotNull; + +import lombok.*; +import nl.tudelft.labracore.api.dto.EditionSummaryDTO; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class QueueEditionSummaryDTO extends EditionSummaryDTO implements QueueEditionViewDTO { + + @NotNull + private Boolean hidden; + +} diff --git a/src/main/java/nl/tudelft/queue/dto/view/QueueEditionViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/QueueEditionViewDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..d2dfb3ce04e76e4160b1dc8b431fa6712cbc0268 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/dto/view/QueueEditionViewDTO.java @@ -0,0 +1,26 @@ +/* + * 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.view; + +public interface QueueEditionViewDTO { + + Long getId(); + + void setHidden(Boolean hidden); + +} diff --git a/src/main/java/nl/tudelft/queue/model/QueueEdition.java b/src/main/java/nl/tudelft/queue/model/QueueEdition.java new file mode 100644 index 0000000000000000000000000000000000000000..797bb9b20aa9ac28a0b633f916ff7b088976f329 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/model/QueueEdition.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.model; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.validation.constraints.NotNull; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QueueEdition { + + @Id + private Long id; + + @NotNull + @Builder.Default + private Boolean hidden = true; + +} diff --git a/src/main/java/nl/tudelft/queue/repository/QueueEditionRepository.java b/src/main/java/nl/tudelft/queue/repository/QueueEditionRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..88b254458d98cd5821d5740996e315c8b6b4c3bc --- /dev/null +++ b/src/main/java/nl/tudelft/queue/repository/QueueEditionRepository.java @@ -0,0 +1,28 @@ +/* + * 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.QueueEdition; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; + +public interface QueueEditionRepository + extends JpaRepository<QueueEdition, Long>, QuerydslPredicateExecutor<QueueEdition> { + +} diff --git a/src/main/java/nl/tudelft/queue/service/EditionService.java b/src/main/java/nl/tudelft/queue/service/EditionService.java index 0fe80542f26985df1d7315e11d052588b54acbc9..64a48bb7c3db34de442c668d840ce7380235d82b 100644 --- a/src/main/java/nl/tudelft/queue/service/EditionService.java +++ b/src/main/java/nl/tudelft/queue/service/EditionService.java @@ -30,6 +30,7 @@ import java.util.zip.ZipOutputStream; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.transaction.Transactional; import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.PersonControllerApi; @@ -41,16 +42,20 @@ import nl.tudelft.queue.csv.EmptyCsvException; import nl.tudelft.queue.csv.InvalidCsvException; import nl.tudelft.queue.csv.UserCsvHelper; import nl.tudelft.queue.dto.util.EditionFilterDTO; +import nl.tudelft.queue.dto.view.QueueEditionViewDTO; import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO; import nl.tudelft.queue.model.LabRequest; +import nl.tudelft.queue.model.QueueEdition; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.embeddables.AllowedRequest; import nl.tudelft.queue.model.enums.QueueSessionType; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.labs.Lab; +import nl.tudelft.queue.repository.QueueEditionRepository; import nl.tudelft.queue.repository.QueueSessionRepository; import org.apache.commons.lang3.StringUtils; +import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.core.io.Resource; @@ -61,6 +66,8 @@ import org.springframework.web.multipart.MultipartFile; @Service public class EditionService { + private static final ModelMapper mapper = new ModelMapper(); + @Autowired private HttpSession session; @@ -76,6 +83,9 @@ public class EditionService { @Autowired private EditionControllerApi eca; + @Autowired + private QueueEditionRepository qer; + @Autowired @Lazy private LabService ls; @@ -86,6 +96,45 @@ public class EditionService { @Autowired private AssignmentCacheManager acm; + /** + * Converts any kind of EditionDTO to a QueueEditionDTO. + * + * @param dto The DTO to convert + * @param qClass The class of QueueEditionDTO to convert to + * @return The converted DTO + */ + public <DTO, QDTO extends QueueEditionViewDTO> QDTO queueEditionDTO(DTO dto, Class<QDTO> qClass) { + return queueEditionDTO(List.of(dto), qClass).get(0); + } + + /** + * Converts a list of any kind of EditionDTOs to QueueEditionDTOs. + * + * @param dtos The DTOs to convert + * @param qClass The class of QueueEditionDTO to convert to + * @return The converted list of DTOs + */ + public <DTO, QDTO extends QueueEditionViewDTO> List<QDTO> queueEditionDTO(List<DTO> dtos, + Class<QDTO> qClass) { + return dtos.stream().map(dto -> { + QDTO qDto = mapper.map(dto, qClass); + QueueEdition qEdition = getOrCreateQueueEdition(qDto.getId()); + qDto.setHidden(qEdition.getHidden()); + return qDto; + }).toList(); + } + + /** + * Gets a queue edition from the database or creates a default one if it does not exist; + * + * @param id The id of the edition + * @return The Queue Edition + */ + @Transactional + public QueueEdition getOrCreateQueueEdition(Long id) { + return qer.findById(id).orElseGet(() -> qer.save(QueueEdition.builder().id(id).build())); + } + /** * Filters a list of people on their username, display name and student number. If any of the * aforementioned values match the given search term, the person is included. If not, the person is diff --git a/src/main/java/nl/tudelft/queue/service/LabService.java b/src/main/java/nl/tudelft/queue/service/LabService.java index 79eea8c1d5f22d102d726949917ec18d5c81f22f..784da1d1bc1b7853aac005f9d897892fc9009730 100644 --- a/src/main/java/nl/tudelft/queue/service/LabService.java +++ b/src/main/java/nl/tudelft/queue/service/LabService.java @@ -44,6 +44,7 @@ import nl.tudelft.queue.dto.create.labs.AbstractSlottedLabCreateDTO; import nl.tudelft.queue.dto.patch.QueueSessionPatchDTO; import nl.tudelft.queue.dto.patch.labs.AbstractSlottedLabPatchDTO; import nl.tudelft.queue.dto.view.CalendarEntryViewDTO; +import nl.tudelft.queue.dto.view.QueueEditionDetailsDTO; import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO; import nl.tudelft.queue.dto.view.requests.SelectionRequestViewDTO; import nl.tudelft.queue.misc.QueueSessionStatus; @@ -90,6 +91,9 @@ public class LabService { @Autowired private SessionCacheManager sCache; + @Autowired + private EditionService es; + @Autowired private EditionControllerApi eApi; @@ -146,7 +150,10 @@ public class LabService { */ public void setOrganizationInModel(SessionDetailsDTO session, Model model) { model.addAttribute("edition", - (session.getEdition() != null) ? eCache.getRequired(session.getEdition().getId()) : null); + (session.getEdition() != null) + ? es.queueEditionDTO(eCache.getRequired(session.getEdition().getId()), + QueueEditionDetailsDTO.class) + : null); model.addAttribute("ec", (session.getEditionCollection() != null) ? ecCache.getRequired(session.getEditionCollection().getId()) diff --git a/src/main/java/nl/tudelft/queue/startup/UnhideEditionsService.java b/src/main/java/nl/tudelft/queue/startup/UnhideEditionsService.java new file mode 100644 index 0000000000000000000000000000000000000000..fed78bb7e5b83ad8607c8ed688b5b175526407ea --- /dev/null +++ b/src/main/java/nl/tudelft/queue/startup/UnhideEditionsService.java @@ -0,0 +1,78 @@ +/* + * 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.startup; + +import javax.transaction.Transactional; + +import nl.tudelft.labracore.api.CourseControllerApi; +import nl.tudelft.labracore.api.EditionControllerApi; +import nl.tudelft.labracore.api.ProgramControllerApi; +import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.queue.model.QueueEdition; +import nl.tudelft.queue.repository.QueueEditionRepository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +/** + * A service that runs during startup. This startup procedure goes through all editions and unhides them in + * Queue. + */ +@Service +@ConditionalOnExpression("#{'${queue.startup.unhide-all-editions}' == 'true'}") +public class UnhideEditionsService { + private static final Logger logger = LoggerFactory.getLogger(UnhideEditionsService.class); + + @Autowired + private QueueEditionRepository qer; + + @Autowired + private EditionControllerApi eApi; + + @Autowired + private CourseControllerApi cApi; + + @Autowired + private ProgramControllerApi pApi; + + @Transactional + @EventListener(ApplicationReadyEvent.class) + public void unhideAllEditions() { + // Going by programme > course > edition to not request to much data + for (ProgramSummaryDTO programme : pApi.getAllPrograms().collectList().block()) { + logger.info("Unhiding courses of programme: " + programme.getName()); + for (CourseSummaryDTO course : cApi.getAllCoursesByProgram(programme.getId()).collectList() + .block()) { + for (EditionDetailsDTO edition : eApi.getAllEditionsByCourse(course.getId()).collectList() + .block()) { + qer.findById(edition.getId()).ifPresentOrElse( + e -> { + e.setHidden(false); + qer.save(e); + }, + () -> qer.save(QueueEdition.builder().id(edition.getId()).hidden(false).build())); + } + } + } + } +} diff --git a/src/main/resources/application.yml.template b/src/main/resources/application.yml.template index 60370803bebb7308e34e18369cb55cf3cf0cbbed..12b9e635f8ab9ad902f3bbf3acd45a26bd4d183d 100644 --- a/src/main/resources/application.yml.template +++ b/src/main/resources/application.yml.template @@ -73,6 +73,7 @@ queue: logo: /img/tudelft-logo.png startup: repair-requests: false + unhide-all-editions: false mail: enabled: true support-email: root diff --git a/src/main/resources/migrations.yml b/src/main/resources/migrations.yml index 958db173e9bbbf78e769c38cc6d46cd9ee669fea..eb73594fccaa2d94dd95d572a96897f929791a03 100644 --- a/src/main/resources/migrations.yml +++ b/src/main/resources/migrations.yml @@ -1045,7 +1045,26 @@ databaseChangeLog: tableName: slotted_lab columnName: previous_empty_allowed_threshold columnDataType: INTEGER - - + # Hide non-queue editions by default + - changeSet: + id: 1684835006964-1 + author: ruben (generated) + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_D + name: id + type: BIGINT + - column: + constraints: + nullable: false + name: hidden + type: BOOLEAN + defaultValueBoolean: true + tableName: queue_edition diff --git a/src/main/resources/templates/edition/view.html b/src/main/resources/templates/edition/view.html index dbaf72e4da9c498023bae261be5a4477658342fc..cfccb53b1fdbaa1c209c3002a422403e046dc710 100644 --- a/src/main/resources/templates/edition/view.html +++ b/src/main/resources/templates/edition/view.html @@ -44,6 +44,21 @@ </nav> </section> + <th:block th:if="${ec == null && (edition instanceof T(nl.tudelft.queue.dto.view.QueueEditionViewDTO)) && edition.hidden && @permissionService.canManageEdition(edition.id)}"> + <div id="edition-hidden-banner" style="cursor: pointer;" class="alert alert-danger mt-md-3" role="alert"> + <span>This edition is not visible to students in Queue. Click on this banner to unhide this edition.</span> + </div> + <form id="edition-unhide-form" method="post" th:action="@{/edition/{editionId}/visibility(editionId=${edition.id})}"></form> + <script th:inline="javascript"> + document.addEventListener("DOMContentLoaded", function () { + const editionUnhideForm = document.getElementById("edition-unhide-form"); + const editionHiddenBanner = document.getElementById("edition-hidden-banner"); + editionHiddenBanner.addEventListener("click", function () { + editionUnhideForm.submit(); + }); + }); + </script> + </th:block> <div class="alert alert-danger mt-md-3" role="alert" th:if="${ec == null && #lists.isEmpty(edition.modules)}"> <span>This edition does not have any modules, create a module first to be able to create sessions.</span> </div> diff --git a/src/main/resources/templates/edition/view/info.html b/src/main/resources/templates/edition/view/info.html index 7db5cbcf8c5bfbe207aa4362f486ea92557d5aa0..c78099e884e716122bef296ae6fe55559c84a04c 100644 --- a/src/main/resources/templates/edition/view/info.html +++ b/src/main/resources/templates/edition/view/info.html @@ -125,6 +125,11 @@ <h3>Options</h3> </div> + <form class="d-inline" th:action="@{/edition/{editionId}/visibility(editionId=${edition.id})}" method="post"> + <button class="btn btn-primary"> + <span th:if="${edition.hidden}">Unh</span><span th:unless="${edition.hidden}">H</span><span>ide in Queue</span> + </button> + </form> <a href="#" th:href="@{/edition/{id}/requests/export(id=${edition.id})}" class="btn btn-primary">Export all labs</a> <a href="#" th:href="@{/edition/{id}/requests/signofflist.csv(id=${edition.id})}" class="btn btn-secondary">Export latest status of student submission requests</a> diff --git a/src/main/resources/templates/home/dashboard.html b/src/main/resources/templates/home/dashboard.html index 5a7587764e700f8f5eccb4b4bd3a94f096bb809c..222999ef4827cd8ac9d7551efccded00969bc7b9 100644 --- a/src/main/resources/templates/home/dashboard.html +++ b/src/main/resources/templates/home/dashboard.html @@ -140,10 +140,14 @@ </div> <div class="tab-pane fade show active" id="overview" role="tabpanel" - aria-labelledby="overview-tabs"> + aria-labelledby="overview-tabs" + th:with="visibleActive = ${activeRoles.?[#this.type.name() == 'TEACHER' or not (#root.editions.get(#this.edition.id).hidden)]}, + visibleUpcoming = ${upcomingRoles.?[#this.type.name() == 'TEACHER' or not (#root.editions.get(#this.edition.id).hidden)]}, + visibleFinished = ${finishedRoles.?[not (#root.editions.get(#this.edition.id).hidden)]}, + visibleArchived = ${archivedRoles.?[not (#root.editions.get(#this.edition.id).hidden)]}"> <div class="boxed-group"> <h3>Active courses you participate in</h3> - <div class="boxed-group-inner" th:if="${#lists.isEmpty(activeRoles)}"> + <div class="boxed-group-inner" th:if="${#lists.isEmpty(visibleActive)}"> <span th:if="${user.defaultRole.name() == 'STUDENT'}"> You do not participate in any courses. Why don't you <a th:href="@{/editions}">enrol for your first course</a>? </span> @@ -153,11 +157,13 @@ </div> <ul class="list-group"> - <li class="list-group-item" th:each="role : ${activeRoles}" - th:with="edition = ${editions.get(role.edition.id)}"> + <li class="list-group-item" th:each="role : ${visibleActive}" + th:with="edition = ${editions.get(role.edition.id)}" + th:if="${role.type.name() == 'TEACHER'} or not ${editions[role.edition.id].hidden}"> <a th:href="@{/edition/{id}(id=${edition.id})}" th:text="|${edition.course.name} (${edition.name})|"></a> <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span> + <span class="badge badge-pill badge-info text-white" th:if="${edition.hidden}">Hidden</span> <ul th:unless="${#lists.isEmpty(labs)}"> <th:block th:each="sess : ${edition.sessions}"> <li th:each="lab : ${labs[sess.id]}" th:unless="${lab.isShared}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'"> @@ -216,24 +222,25 @@ </ul> </div> - <div class="boxed-group" th:if="${!#lists.isEmpty(upcomingRoles)}"> + <div class="boxed-group" th:if="${!#lists.isEmpty(visibleUpcoming)}"> <h3>Upcoming courses</h3> <ul class="list-group"> - <li class="list-group-item" th:each="role : ${upcomingRoles}" + <li class="list-group-item" th:each="role : ${visibleUpcoming}" th:with="edition = ${editions.get(role.edition.id)}"> <a th:href="@{/edition/{id}(id=${edition.id})}" th:text="|${edition.course.name} (${edition.name})|"></a> <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span> + <span class="badge badge-pill badge-info text-white" th:if="${edition.hidden}">Hidden</span> </li> </ul> </div> - <div class="boxed-group" th:if="${!#lists.isEmpty(finishedRoles)}"> + <div class="boxed-group" th:if="${!#lists.isEmpty(visibleFinished)}"> <h3>Finished courses</h3> <ul class="list-group"> - <li class="list-group-item" th:each="role : ${finishedRoles}" + <li class="list-group-item" th:each="role : ${visibleFinished}" th:with="edition = ${editions.get(role.edition.id)}"> <a th:href="@{/edition/{id}(id=${edition.id})}" th:text="|${edition.course.name} (${edition.name})|"></a> @@ -242,11 +249,11 @@ </ul> </div> - <div class="boxed-group" th:if="${!#lists.isEmpty(archivedRoles)}"> + <div class="boxed-group" th:if="${!#lists.isEmpty(visibleArchived)}"> <h3>Archived courses</h3> <ul class="list-group"> - <th:block th:each="role : ${archivedRoles}" th:with="edition = ${editions.get(role.edition.id)}"> + <th:block th:each="role : ${visibleArchived}" th:with="edition = ${editions.get(role.edition.id)}"> <li class="list-group-item"> <a th:href="@{/edition/{id}(id=${edition.id})}" th:text="|${edition.course.name} (${edition.name})|"></a> diff --git a/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java b/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java index a5598ca589e939e7b3a36a743bfa053ab8ce755d..260ba0e1bb03b0475ed75c057753823d2e1fb8c3 100644 --- a/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java +++ b/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java @@ -17,6 +17,7 @@ */ package nl.tudelft.queue.controller; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,12 +34,14 @@ import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.dto.EditionDetailsDTO; import nl.tudelft.labracore.api.dto.EditionSummaryDTO; import nl.tudelft.labracore.api.dto.Id; -import nl.tudelft.labracore.api.dto.PageEditionDetailsDTO; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; import nl.tudelft.labracore.api.dto.RoleDetailsDTO; import nl.tudelft.labracore.api.dto.SessionDetailsDTO; import nl.tudelft.queue.dto.util.EditionFilterDTO; +import nl.tudelft.queue.dto.view.QueueEditionDetailsDTO; import nl.tudelft.queue.model.labs.Lab; +import nl.tudelft.queue.repository.QueueEditionRepository; +import nl.tudelft.queue.service.EditionService; import org.hamcrest.core.StringContains; import org.junit.jupiter.api.BeforeEach; @@ -99,6 +102,12 @@ public class EditionControllerTest { @Autowired private StudentGroupApiMocker sgApiMocker; + @Autowired + private QueueEditionRepository qer; + + @Autowired + private EditionService es; + private EditionDetailsDTO oopNow; private SessionDetailsDTO session1; private Lab lab1; @@ -128,8 +137,8 @@ public class EditionControllerTest { @Test @WithUserDetails("teacher0") void getEmptyCourseListContainsModelAttributes() throws Exception { - when(eApi.getEditionsPageActiveOrTaughtBy(any(), any(), any(), any())) - .thenReturn(Mono.just(new PageEditionDetailsDTO().content(List.of()).totalElements(0L))); + when(eApi.getAllEditionsActiveOrTaughtBy(any())) + .thenReturn(Flux.just()); when(eApi.getAllEditionsActiveDuringPeriod(any())).thenReturn(Flux.just()); @@ -156,8 +165,8 @@ public class EditionControllerTest { @Test @WithUserDetails("teacher0") void submitFiltersRemembersFilter() throws Exception { - when(eApi.getEditionsPageActiveOrTaughtBy(any(), any(), any(), any())) - .thenReturn(Mono.just(new PageEditionDetailsDTO().content(List.of()).totalElements(0L))); + when(eApi.getAllEditionsActiveOrTaughtBy(any())) + .thenReturn(Flux.just()); when(eApi.getAllEditionsActiveDuringPeriod(any())).thenReturn(Flux.just()); @@ -185,8 +194,8 @@ public class EditionControllerTest { @Test @WithUserDetails("student155") void allEditionsNotExposedToStudent() throws Exception { - when(eApi.getEditionsPageActiveOrTaughtBy(any(), any(), any(), any())) - .thenReturn(Mono.just(new PageEditionDetailsDTO().content(List.of()).totalElements(0L))); + when(eApi.getAllEditionsActiveOrTaughtBy(any())) + .thenReturn(Flux.just()); when(eApi.getAllEditionsActiveDuringPeriod(any())).thenReturn(Flux.just()); @@ -245,7 +254,8 @@ public class EditionControllerTest { void everyoneShouldBeAbleToSeeEditionInfo() throws Exception { mvc.perform(get("/edition/{id}", oopNow.getId())) .andExpect(status().isOk()) - .andExpect(model().attribute("edition", oopNow)); + .andExpect(model().attribute("edition", + es.queueEditionDTO(oopNow, QueueEditionDetailsDTO.class))); } @Test @@ -309,6 +319,17 @@ public class EditionControllerTest { .andExpect(flash().attributeExists("error")); } + @Test + @WithUserDetails("admin") + void toggleEdition() throws Exception { + mvc.perform(post("/edition/1/visibility").with(csrf())) + .andExpect(status().is3xxRedirection()); + assertThat(qer.findById(1L)).isPresent().hasValueSatisfying(e -> assertThat(e.getHidden()).isFalse()); + mvc.perform(post("/edition/1/visibility").with(csrf())) + .andExpect(status().is3xxRedirection()); + assertThat(qer.findById(1L)).isPresent().hasValueSatisfying(e -> assertThat(e.getHidden()).isTrue()); + } + @ParameterizedTest @MethodSource(value = "protectedEndpoints") void testWithoutUserDetailsIsForbidden(MockHttpServletRequestBuilder request) throws Exception { @@ -335,6 +356,7 @@ public class EditionControllerTest { post("/edition/1/participants/1/block"), get("/edition/1/leave"), post("/edition/1/leave"), + post("/edition/1/toggle"), get("/edition/1/status")); } }