diff --git a/CHANGELOG.md b/CHANGELOG.md index 92373a7b07dc3ea67bbce4898c9f665a95652fad..b3fb342b8cd20063f774ea5dd8d0c2e8ff9fd823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [2.1.2] +### Added + - New history page that shows the handled requests in the current session. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Added course code to request details page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + +### Changed + - Colours used in request tables are now more colour blind friendly. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Requests page now only shows pending and forwarded requests. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Request filters are now a dialog. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - New requests now appear at the top of the requests page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + +### Fixed + - Request table no longer updates when not on the first page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + +### Deprecated + +### Removed + - Refresh button on requests page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Student name in requests table on the requests page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + ## [2.1.1] ### Added - Added upcoming courses to home page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) diff --git a/build.gradle.kts b/build.gradle.kts index 2a26978c0699f196996252a408bacda1cc892ff3..3787b68717f9528f864260333940b844f9c8efa6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ import com.diffplug.gradle.spotless.SpotlessExtension import org.springframework.boot.gradle.tasks.run.BootRun group = "nl.tudelft.ewi.queue" -version = "2.1.1" +version = "2.1.2" val javaVersion = JavaVersion.VERSION_17 @@ -350,8 +350,8 @@ dependencies { implementation("org.webjars:chartjs:2.7.0") implementation("org.webjars:tempusdominus-bootstrap-4:5.1.2") implementation("org.webjars:momentjs:2.24.0") - implementation("org.webjars:codemirror:5.50.0") implementation("org.webjars:fullcalendar:5.9.0") + implementation("org.webjars:codemirror:5.62.2") // Library for converting markdown to html implementation("org.commonmark:commonmark:0.18.1") diff --git a/src/main/java/nl/tudelft/queue/controller/LabController.java b/src/main/java/nl/tudelft/queue/controller/LabController.java index 083a3e3257c1bfc254b82ec8041b7064a1b08b43..259078aa0400b063860fbce1380d299ead2b5b2d 100644 --- a/src/main/java/nl/tudelft/queue/controller/LabController.java +++ b/src/main/java/nl/tudelft/queue/controller/LabController.java @@ -38,6 +38,8 @@ import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.librador.resolver.annotations.PathEntity; import nl.tudelft.queue.cache.*; +import nl.tudelft.queue.csv.EmptyCsvException; +import nl.tudelft.queue.csv.InvalidCsvException; import nl.tudelft.queue.dto.create.labs.CapacitySessionCreateDTO; import nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO; import nl.tudelft.queue.dto.create.labs.RegularLabCreateDTO; @@ -72,6 +74,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import com.google.common.net.HttpHeaders; @@ -661,6 +664,23 @@ public class LabController { .body(resource); } + /** + * Imports a list of students which should get priority for reviewing during an exam lab. + * + * @param session The exam lab to which they should be added. + * @param csv The csv file containing NetIDs for these students. + * @return + * @throws EmptyCsvException + * @throws InvalidCsvException + */ + @PostMapping("/lab/{session}/import") + @PreAuthorize("@permissionService.canManageSession(#session)") + public String importStudents(@PathEntity ExamLab session, + @RequestParam("file") MultipartFile csv) throws EmptyCsvException, InvalidCsvException { + ls.addStudentsToReview(session, csv); + return "redirect:/lab/" + session.getId(); + } + /** * Sets the model attributes for an enqueueing or editing requests page. * diff --git a/src/main/java/nl/tudelft/queue/controller/RequestController.java b/src/main/java/nl/tudelft/queue/controller/RequestController.java index fed7995e1da37a7f854fc7bed91c4e175616a8f8..f4fa420ebab40237c77df9dc8bb88b5d8ccf184b 100644 --- a/src/main/java/nl/tudelft/queue/controller/RequestController.java +++ b/src/main/java/nl/tudelft/queue/controller/RequestController.java @@ -22,6 +22,7 @@ import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.transaction.Transactional; @@ -136,7 +137,34 @@ public class RequestController { public String getRequestTableView(Model model, @AuthenticatedPerson Person assistant, @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC, size = 25) Pageable pageable) { - var filter = rts.checkAndStoreFilterDTO(null, "/requests"); + setRequestTableAttributes(model, pageable, assistant, + "/requests", r -> r.getEventInfo().getStatus().isPending(), false, true); + return "request/list"; + } + + /** + * Gets the request table history view page. This page should be accessible to assistants, TAs, teachers, + * etc., but not for regular students. This page displays a big table containing all current requests. + * + * @param pageable The pageable determining the current size and page of the view. + * @param model The model to fill out for Thymeleaf template resolution. + * @return The Thymeleaf template to resolve. + */ + @GetMapping("/requests/history") + @PreAuthorize("@permissionService.canViewRequests()") + public String getRequestTableHistoryView(Model model, + @AuthenticatedPerson Person assistant, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC, size = 25) Pageable pageable) { + setRequestTableAttributes(model, pageable, assistant, + "/requests/history", r -> !r.getEventInfo().getStatus().isPending(), true, false); + return "request/list"; + } + + private void setRequestTableAttributes(Model model, Pageable pageable, Person assistant, + String filterPath, + Predicate<LabRequest> requestFilter, + boolean reversed, boolean forwardedFirst) { + var filter = rts.checkAndStoreFilterDTO(null, filterPath); List<QueueSession<?>> qSessions = rts.addFilterAttributes(model, null); List<Lab> labs = qSessions.stream() @@ -145,15 +173,16 @@ public class RequestController { .collect(Collectors.toList()); List<LabRequest> filteredRequests = rs - .filterRequestsSharedEditionCheck(lrr.findAllByFilter(labs, filter)); + .filterRequestsSharedEditionCheck(lrr.findAllByFilter(labs, filter)) + .stream().filter(requestFilter) + .toList(); model.addAttribute("page", "requests"); model.addAttribute("filter", filter); - model.addAttribute("requests", rts.convertRequestsToView(filteredRequests, pageable)); + model.addAttribute("requests", + rts.convertRequestsToView(filteredRequests, pageable, reversed, forwardedFirst)); model.addAttribute("requestCounts", rts.labRequestCounts( labs, assistant, filter)); - - return "request/list"; } /** diff --git a/src/main/java/nl/tudelft/queue/csv/CsvPerson.java b/src/main/java/nl/tudelft/queue/csv/CsvPerson.java new file mode 100644 index 0000000000000000000000000000000000000000..293c9965094f13398c73a9b0064ec929356e5dad --- /dev/null +++ b/src/main/java/nl/tudelft/queue/csv/CsvPerson.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.csv; + +import java.util.List; + +import lombok.Getter; + +import org.springframework.web.multipart.MultipartFile; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.csv.CsvSchema; + +@Getter +public class CsvPerson { + + private static final CsvSchema schema = CsvSchema.builder().setSkipFirstDataRow(true) + .addColumn("username").build(); + + private String username; + + @JsonCreator + public CsvPerson(@JsonProperty("username") String username) { + this.username = username; + } + + public static List<CsvPerson> readCsv(MultipartFile csv) throws EmptyCsvException, InvalidCsvException { + return CsvHelper.readCsv(csv, CsvPerson.class, schema); + } +} diff --git a/src/main/java/nl/tudelft/queue/realtime/messages/RequestForwardedToAnyMessage.java b/src/main/java/nl/tudelft/queue/realtime/messages/RequestForwardedToAnyMessage.java index 7a0eaf19633982d778c08677c0a30ebca365cff7..2426b2838a52d9026d0cca55dcd0cd1defedebd8 100644 --- a/src/main/java/nl/tudelft/queue/realtime/messages/RequestForwardedToAnyMessage.java +++ b/src/main/java/nl/tudelft/queue/realtime/messages/RequestForwardedToAnyMessage.java @@ -20,6 +20,8 @@ package nl.tudelft.queue.realtime.messages; import java.time.LocalDateTime; import lombok.*; +import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.events.RequestForwardedToAnyEvent; @@ -33,6 +35,21 @@ public class RequestForwardedToAnyMessage extends RequestStatusUpdateMessage<Req private LocalDateTime forwardedAt; + private String requestedBy; + + private String organizationName; + + private String roomName; + private String buildingName; + + private OnlineMode onlineMode; + private String onlineModeDisplayName; + + private String assignmentName; + private String moduleName; + + private String requestTypeDisplayName; + @Override public RequestStatus getStatus() { return RequestStatus.FORWARDED; @@ -48,5 +65,24 @@ public class RequestForwardedToAnyMessage extends RequestStatusUpdateMessage<Req super.postApply(); forwardedAt = data.getTimestamp(); + + LabRequestViewDTO view = data.getRequest().toViewDTO(); + + organizationName = view.organizationName(); + + requestedBy = view.requesterEntityName(); + + if (view.getRoom() != null) { + roomName = view.getRoom().getName(); + buildingName = view.getRoom().getBuilding().getName(); + } else if (view.getOnlineMode() != null) { + onlineMode = view.getOnlineMode(); + onlineModeDisplayName = view.getOnlineMode().getDisplayName(); + } + + assignmentName = view.getAssignment().getName(); + moduleName = view.getAssignment().getModule().getName(); + + requestTypeDisplayName = view.getRequestType().displayName(); } } diff --git a/src/main/java/nl/tudelft/queue/realtime/messages/RequestForwardedToPersonMessage.java b/src/main/java/nl/tudelft/queue/realtime/messages/RequestForwardedToPersonMessage.java index 3ad00d801e559315785c4981b23469bc405882df..7e9ea201f4548c92246bf0e917c93c5a892ad7bf 100644 --- a/src/main/java/nl/tudelft/queue/realtime/messages/RequestForwardedToPersonMessage.java +++ b/src/main/java/nl/tudelft/queue/realtime/messages/RequestForwardedToPersonMessage.java @@ -18,6 +18,8 @@ package nl.tudelft.queue.realtime.messages; import lombok.*; +import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO; +import nl.tudelft.queue.model.enums.OnlineMode; import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.events.RequestForwardedToPersonEvent; @@ -32,6 +34,21 @@ public class RequestForwardedToPersonMessage private Long forwardedTo; + private String requestedBy; + + private String organizationName; + + private String roomName; + private String buildingName; + + private OnlineMode onlineMode; + private String onlineModeDisplayName; + + private String assignmentName; + private String moduleName; + + private String requestTypeDisplayName; + @Override public RequestStatus getStatus() { return RequestStatus.FORWARDED; @@ -47,5 +64,24 @@ public class RequestForwardedToPersonMessage super.postApply(); forwardedTo = data.getForwardedTo(); + + LabRequestViewDTO view = data.getRequest().toViewDTO(); + + organizationName = view.organizationName(); + + requestedBy = view.requesterEntityName(); + + if (view.getRoom() != null) { + roomName = view.getRoom().getName(); + buildingName = view.getRoom().getBuilding().getName(); + } else if (view.getOnlineMode() != null) { + onlineMode = view.getOnlineMode(); + onlineModeDisplayName = view.getOnlineMode().getDisplayName(); + } + + assignmentName = view.getAssignment().getName(); + moduleName = view.getAssignment().getModule().getName(); + + requestTypeDisplayName = view.getRequestType().displayName(); } } diff --git a/src/main/java/nl/tudelft/queue/service/LabService.java b/src/main/java/nl/tudelft/queue/service/LabService.java index efae7c7c1481bba5508bea30e44919361186524a..252be4acbf8325bd3a3bc0f8b6e1653e7554b9fa 100644 --- a/src/main/java/nl/tudelft/queue/service/LabService.java +++ b/src/main/java/nl/tudelft/queue/service/LabService.java @@ -29,6 +29,7 @@ import javax.transaction.Transactional; import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.ModuleControllerApi; +import nl.tudelft.labracore.api.PersonControllerApi; import nl.tudelft.labracore.api.SessionControllerApi; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.lib.security.user.Person; @@ -36,7 +37,7 @@ import nl.tudelft.librador.dto.view.View; import nl.tudelft.queue.cache.EditionCacheManager; import nl.tudelft.queue.cache.EditionCollectionCacheManager; import nl.tudelft.queue.cache.SessionCacheManager; -import nl.tudelft.queue.csv.CsvHelper; +import nl.tudelft.queue.csv.*; import nl.tudelft.queue.dto.create.QueueSessionCreateDTO; import nl.tudelft.queue.dto.create.labs.AbstractSlottedLabCreateDTO; import nl.tudelft.queue.dto.patch.QueueSessionPatchDTO; @@ -52,6 +53,7 @@ import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.enums.SelectionProcedure; import nl.tudelft.queue.model.labs.AbstractSlottedLab; 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.properties.QueueProperties; @@ -63,6 +65,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import org.springframework.ui.Model; +import org.springframework.web.multipart.MultipartFile; @Service public class LabService { @@ -112,6 +115,9 @@ public class LabService { @Autowired private ModuleControllerApi mApi; + @Autowired + private PersonControllerApi pca; + public enum SessionType { REGULAR, SHARED @@ -493,4 +499,21 @@ public class LabService { return labs.stream().map(Lab::getOnlineModes).flatMap(Set::stream).collect(Collectors.toSet()); } + /** + * Adds students, identified by their netid in a CSV file to an exam lab to give them priority during the + * review. + * + * @param exam The exam to which they should be added. + * @param file The csv file containing netid's + * @throws EmptyCsvException + * @throws InvalidCsvException + */ + @Transactional + public void addStudentsToReview(ExamLab exam, MultipartFile file) + throws EmptyCsvException, InvalidCsvException { + List<PersonDetailsDTO> persons = CsvPerson.readCsv(file).stream() + .map(s -> pca.getPersonByUsername(s.getUsername()).block()).toList(); + exam.setPickedStudents(persons.stream().map(PersonDetailsDTO::getId).collect(Collectors.toSet())); + } + } diff --git a/src/main/java/nl/tudelft/queue/service/RequestTableService.java b/src/main/java/nl/tudelft/queue/service/RequestTableService.java index 4d54517175ddd55c632244c9ed56483c0eeb4e58..6e311d9f3f4d8398c772729015e6cb2e5d2601ee 100644 --- a/src/main/java/nl/tudelft/queue/service/RequestTableService.java +++ b/src/main/java/nl/tudelft/queue/service/RequestTableService.java @@ -23,6 +23,7 @@ import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; import javax.servlet.http.HttpSession; @@ -38,6 +39,7 @@ import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO; import nl.tudelft.queue.dto.view.RequestViewDTO; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.Request; +import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.labs.AbstractSlottedLab; import nl.tudelft.queue.model.labs.Lab; import nl.tudelft.queue.repository.LabRepository; @@ -243,14 +245,25 @@ public class RequestTableService { * Converts a list of requests to a view. Sorts before conversion. Work around for PageImpl not supporting * List to page conversions. * - * @param requestList The list to convert to a page view - * @param pageable The pageable object - * @return The Page of request views. + * @param requestList The list to convert to a page view + * @param pageable The pageable object + * @param reversed Sorts by reversed order of creation time instead + * @param forwardedFirst Puts forwarded requests at the start + * @return The Page of request views. */ public Page<RequestViewDTO<?>> convertRequestsToView(List<? extends Request<?>> requestList, - Pageable pageable) { + Pageable pageable, boolean reversed, boolean forwardedFirst) { + Comparator<Request<?>> comparator = Comparator.comparing(Request::getCreatedAt); var sortedRequestList = requestList.stream() - .sorted(Comparator.comparing((Request<?> r) -> r.getCreatedAt()).reversed()).toList(); + .sorted(reversed ? comparator.reversed() : comparator).toList(); + if (forwardedFirst) { + sortedRequestList = Stream.concat( + sortedRequestList.stream() + .filter(r -> r.getEventInfo().getStatus() == RequestStatus.FORWARDED), + sortedRequestList.stream() + .filter(r -> r.getEventInfo().getStatus() != RequestStatus.FORWARDED)) + .toList(); + } final int start = (int) pageable.getOffset(); final int end = (int) (Math.min((start + pageable.getPageSize()), requestList.size())); return new PageImpl<>(convertRequestsToView(sortedRequestList.subList(start, end)), pageable, diff --git a/src/main/resources/scss/_variables.scss b/src/main/resources/scss/_variables.scss index fe3c3d36fa7568462626c117a9be661745a6a9ff..9d39cbffaf6d66eee39b8313217c2f0688b30793 100644 --- a/src/main/resources/scss/_variables.scss +++ b/src/main/resources/scss/_variables.scss @@ -32,13 +32,14 @@ $navbar-tabs-border: #dee2e6; $navbar-tabs-background: #ffffff; // Background colours requests -$background-revoked: $bs-secondary; -$background-processing: $bs-info; -$background-pending: $bs-primary; -$background-approved: $bs-success; -$background-forwarded: $bs-primary; -$background-rejected: $bs-danger; -$background-notfound: $bs-warning; +$background-request-opacity: 0.75; +$background-revoked: hsla(0, 0%, 30%, $background-request-opacity); +$background-processing: hsla(240, 50%, 40%, $background-request-opacity); +$background-pending: hsla(240, 70%, 40%, $background-request-opacity); +$background-approved: hsla(140, 75%, 30%, $background-request-opacity); +$background-forwarded: hsla(260, 70%, 40%, $background-request-opacity); +$background-rejected: hsla(0, 90%, 40%, $background-request-opacity); +$background-notfound: hsla(40, 70%, 45%, $background-request-opacity); $background-notpicked: #999e9e; // Queue page background colours diff --git a/src/main/resources/static/js/request_table.js b/src/main/resources/static/js/request_table.js index 74ff96666accf63edbf90ae253c8e35bb39509cd..641a5c018494b3dc46e5568d9b45aa7ced7b99b8 100644 --- a/src/main/resources/static/js/request_table.js +++ b/src/main/resources/static/js/request_table.js @@ -75,25 +75,32 @@ const inFilter = (() => { */ function handleSocketCreation(client) { client.subscribe("/user/topic/request-table", msg => { + const url = new URL(window.location); + const page = url.searchParams.get("page"); + if (page && page > 0) return; + const event = JSON.parse(msg.body); if (event.type === "request-created" && inFilter(event)) { - prependToRequestTable(event); + appendToRequestTable(event); increaseGetNextCounter(event["labId"]) } else if (event.type !== "request-created") { - updateStatus(event["id"], event["status"]); switch (event.type) { case "request-taken": decreaseGetNextCounter(event["labId"]); updateAssigned(event["id"], event["takenBy"]); + removeFromRequestTable(event["id"]); break; case "request-revoked": decreaseGetNextCounter(event["labId"]); + removeFromRequestTable(event["id"]); break; case "request-forwarded-to-any": + prependToRequestTable(event); increaseGetNextCounter(event["labId"]); break; case "request-forwarded-to-person": if (event["forwardedTo"] === authenticatedId) { + prependToRequestTable(event); increaseGetNextCounter(event["labId"]); } break; @@ -118,9 +125,45 @@ function prependToRequestTable(event) { // Add the compiled HTML to the request table with a fade in effect. $(html).hide().prependTo("#request-table tbody").fadeIn(); + + $("#no-requests-info").hide(); +} + +/** + * Use Handlebars to compile a template for each request and fill in the template directly using the request + * info sent through the web socket message. + * @param event The event that occured with information on the created request. + */ +function appendToRequestTable(event) { + // Get the request template and fill it in + const source = $("#request-entry-template").html(); + const template = Handlebars.compile(source); + const html = template(event); + + // Add the compiled HTML to the request table with a fade in effect. + $(html).hide().appendTo("#request-table tbody").fadeIn(); + + $("#no-requests-info").hide(); } /** + * Removed a request from the request table. + * @param id {number} The id of the request to remove. + */ +function removeFromRequestTable(id) { + const rowSelector = selectRow(id); + rowSelector.fadeOut(); + setTimeout(() => { + rowSelector.remove(); + + const amtRequests = $("tr[id^='request-']").length; + if (amtRequests === 0) { + $("#no-requests-info").show(); + } + }, 500); +} + + /** * Updates the status of a request with the given id in the request table to the given status. * @param id {number} The id of the request to update. * @param status {string} The status to update the request to. diff --git a/src/main/resources/templates/history/index.html b/src/main/resources/templates/history/index.html index 14f072e6dcd2c2c502892eeb1054bad53b77fa14..68a21b7a648e274fd59e22d22a73e05eb5c1a86b 100644 --- a/src/main/resources/templates/history/index.html +++ b/src/main/resources/templates/history/index.html @@ -35,21 +35,18 @@ <div class="page-header"> <h1>My Requests</h1> </div> + + <th:block th:replace="request/list/filters :: filters (returnPath='/history')"> + </th:block> </section> <section layout:fragment="outside-content"> - <div class="row no-gutters"> - <div class="col-lg-3 col-xl-2 pl-lg-3 pr-lg-1"> - <th:block th:replace="request/list/filters :: filters (returnPath=@{/history})"> - </th:block> - </div> - <div class="col-lg-9 col-xl-10 pl-lg-1 pr-lg-3"> - <th:block th:replace="request/list/request-table :: request-table"> - </th:block> - </div> + <div class="row mx-xl-5 mx-sm-3"> + <th:block th:replace="request/list/request-table :: request-table(showName=true, showOnlyRelevant=${false})"> + </th:block> </div> - <div class="justify-content-center"> + <div class="row mx-xl-5 mx-sm-3"> <th:block th:replace="pagination :: pagination (page=${requests}, size=3)"> </th:block> </div> diff --git a/src/main/resources/templates/history/student.html b/src/main/resources/templates/history/student.html index e18dada895a747a7c90daab0b0794e9a9b72eed4..df96cee050f188555113f198c81f553b8205d8bc 100644 --- a/src/main/resources/templates/history/student.html +++ b/src/main/resources/templates/history/student.html @@ -37,21 +37,18 @@ <div class="page-header"> <h1>Requests of <span th:text="${student.displayName}"></span></h1> </div> + + <th:block th:replace="request/list/filters :: filters (returnPath=@{/history/course/{editionId}/student/{studentId}(editionId=${edition.id}, studentId=${student.id})})"> + </th:block> </section> <section layout:fragment="outside-content"> - <div class="row no-gutters"> - <div class="col-lg-3 col-xl-2 pl-lg-3 pr-lg-1"> - <th:block th:replace="request/list/filters :: filters (returnPath=@{/history/course/{editionId}/student/{studentId}(editionId=${edition.id}, studentId=${student.id})})"> - </th:block> - </div> - <div class="col-lg-9 col-xl-10 pl-lg-1 pr-lg-3"> - <th:block th:replace="request/list/request-table :: request-table"> - </th:block> - </div> + <div class="row mx-xl-5 mx-sm-3"> + <th:block th:replace="request/list/request-table :: request-table(showName=true, showOnlyRelevant=${false})"> + </th:block> </div> - <div class="justify-content-center"> + <div class="row mx-xl-5 mx-sm-3"> <th:block th:replace="pagination :: pagination (page=${requests}, size=3)"> </th:block> </div> diff --git a/src/main/resources/templates/lab/view.html b/src/main/resources/templates/lab/view.html index 84dcdb866379ddeeff9c89b0ea9a5c92af796944..8b3b8f0a3a6ec1ca21316f99cb5615e60f5f4444 100644 --- a/src/main/resources/templates/lab/view.html +++ b/src/main/resources/templates/lab/view.html @@ -112,6 +112,8 @@ </th:block> </div> + <th:block layout:fragment="exam-upload-students"></th:block> + <div class="mt-3"> <th:block layout:fragment="request-table"> </th:block> diff --git a/src/main/resources/templates/lab/view/components/exam-lab-info.html b/src/main/resources/templates/lab/view/components/exam-lab-info.html index ea5fcd67cfb3286a572ac1bb740c7d8412684f47..09943a143e8f6c7e1a38bc5200a59eda75869fd5 100644 --- a/src/main/resources/templates/lab/view/components/exam-lab-info.html +++ b/src/main/resources/templates/lab/view/components/exam-lab-info.html @@ -40,7 +40,7 @@ <dt>Minimum Exam lab Percentage</dt> <dd th:text="${qSession.examLabConfig.percentage + '%'}"></dd> <th:block th:if="${@permissionService.canManageSession(qSession.data)}"> - <dt>Progress </dt> + <dt>Progress</dt> <dd> <div class="progress"> <div class="progress-bar bg-success" @@ -48,7 +48,8 @@ </div> </dd> <dd th:text="|Handled ${qSession.totalHandled} out of - ${qSession.totalNeeded} required students|"/></dd> + ${qSession.totalNeeded} required students|"/> + </dd> </th:block> </dl> </div> diff --git a/src/main/resources/templates/lab/view/components/exam-upload-students.html b/src/main/resources/templates/lab/view/components/exam-upload-students.html new file mode 100644 index 0000000000000000000000000000000000000000..73bfae690129ebb489ce4fcfa614d44680ec5e9d --- /dev/null +++ b/src/main/resources/templates/lab/view/components/exam-upload-students.html @@ -0,0 +1,48 @@ +<body> +<th:block th:fragment="exam-upload-students"> + <div class="card lab-info row mt-3 mx-0"> + <script type="text/javascript" src="/webjars/codemirror/5.62.2/lib/codemirror.js"></script> + <link rel="stylesheet" type="text/css" href="/webjars/codemirror/5.62.2/lib/codemirror.css" /> + <h3 class="card-header">Force students to be reviewed</h3> + <div class="card-body"><p class="lead">These students will be given priority when doing exam reviews</p> + <form action="#" + th:action="@{/lab/{editionId}/import(editionId=${qSession.id})}" + enctype="multipart/form-data" class="form-horizontal" method="post"> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-8"> + <label class="custom-file"> + <input type="file" id="csv" name="file" class="csvfile"/> + <span class="csvfile"></span> + </label> + </div> + </div> + + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-8"> + <button type="submit" class="btn btn-primary" + name="storeTA">Import students + </button> + </div> + </div> + <div class="col-sm-8"> + The CSV should look like: <br> + <div id="csvExample"></div> + </div> + <script> + $(function () { + let csvExample = CodeMirror(document.getElementById('csvExample'), { + value: + " name \n netid1\n netid2", + mode: "mathematica", + lineNumbers: true, + readOnly: true + }); + csvExample.setSize("100%", "100%") + + }); + </script> + </form> + </div> + </div> +</th:block> +</body> diff --git a/src/main/resources/templates/lab/view/exam.html b/src/main/resources/templates/lab/view/exam.html index 79eba6e8ac877e7ab63ae95e6c943af03f4bcf8c..8d73044a4f4025712235f80bec3540aeebe91059 100644 --- a/src/main/resources/templates/lab/view/exam.html +++ b/src/main/resources/templates/lab/view/exam.html @@ -39,7 +39,10 @@ <th:block th:replace="lab/view/components/exam-lab-info :: exam-lab-info"> </th:block> </th:block> - +<th:block th:if="${@permissionService.canManageSession(qSession.data)}" layout:fragment="exam-upload-students"> + <th:block th:replace="lab/view/components/exam-upload-students :: exam-upload-students"> + </th:block> +</th:block> <th:block th:if="${@permissionService.canTakeRequest(qSession.id)}" layout:fragment="exam-request-table"> <th:block th:replace="lab/view/components/exam-lab-slots :: exam-lab-slots"> </th:block> diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index c84a36be7cec9d444f53e235e585585fd9caecfe..428b81e5294866d527234ab5330c4afde13ad4b3 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -148,7 +148,7 @@ <div class="row no-gutters justify-content-center mb-3"> <!-- Page content --> - <div class="pl-3 pr-3 col-12 col-sm-11 col-md-10 col-lg-9 col-xl-8" style="max-width: 75rem;"> + <div class="pl-3 pr-3 col-12" style="max-width: 75rem;"> <th:block layout:fragment="content" class="content"> <p>Page content goes here</p> </th:block> @@ -175,7 +175,7 @@ <footer class="bg-light"> <div class="container-fluid"> <div class="row no-gutters justify-content-center"> - <div class="col-12 col-lg-9 col-xl-8 mx-auto" style="max-width: 75rem;"> + <div class="col-12 mx-auto" style="max-width: 75rem;"> <div class="row justify-content-center"> <div class="d-none d-sm-block col-sm-6"> <p class="text-muted btn mb-0">© Delft University of Technology</p> diff --git a/src/main/resources/templates/request/list.html b/src/main/resources/templates/request/list.html index 9a1e67069eec968122872aef9c834d6ca01a0f8c..7d360ccbb5877afbe034a2f7ce0687d4ae877636 100644 --- a/src/main/resources/templates/request/list.html +++ b/src/main/resources/templates/request/list.html @@ -35,7 +35,7 @@ <head> <title>Requests</title> - <script type="text/javascript" src="/js/request_table.js"></script> + <script th:unless="${#request.requestURI.matches('.*/history.*')}" type="text/javascript" src="/js/request_table.js"></script> <script type="text/javascript" src="/webjars/handlebars/handlebars.min.js"></script> <link th:if="${@thymeleafConfig.isTheDay() && @requestTableService.partakes()}" rel="stylesheet" type="text/css" href="/css/stack.css"/> @@ -50,11 +50,31 @@ </ol> </nav> + <ul class="nav nav-tabs mb-3"> + <li class="nav-item"> + <a href="#" class="nav-link" + th:classappend="${#request.requestURI.matches('.*/history.*') ? '' : 'active'}" + th:href="@{/requests}"> + <i class="fa fa-question" aria-hidden="true"></i> Current requests + </a> + </li> + <li class="nav-item"> + <a href="#" class="nav-link" + th:classappend="${#request.requestURI.matches('.*/history.*') ? 'active' : ''}" + th:href="@{/requests/history}"> + <i class="fa fa-history" aria-hidden="true"></i> History + </a> + </li> + </ul> + <div class="row"> <h1 class="col-12">Requests</h1> </div> - <div class="row mb-2"> + <th:block th:replace="request/list/filters :: filters (returnPath=${#request.requestURI.matches('.*/history.*') ? '/requests/history' : '/requests'})"> + </th:block> + + <div class="row mb-3" th:unless="${#request.requestURI.matches('.*/history.*')}"> <div class="col-12 btn-toolbar float-right" role="toolbar"> <th:block th:each="lab : ${labs}"> <a type="submit" class="btn btn-sm btn-get-next text-white mr-1 float-right" @@ -67,8 +87,6 @@ <span th:id="|span-${lab.id}|" th:text="|(${requestCounts.getOrDefault(lab.id, 0)})|">(0)</span> </a> </th:block> - <a class="btn btn-sm btn-secondary text-white float-right" - onclick='location.reload();'>Refresh</a> <form th:if="${@thymeleafConfig.isTheDay() && @requestTableService.partakes()}" method="post" th:action="@{/requests/ilikemyeyesthankyou}"> <button type="submit" @@ -82,33 +100,12 @@ </div> </div> - <form class="form" method="get"> - <div class="form-row"> - <input class="form-control col-sm-5 mr-0 mr-sm-2 mb-2 mb-sm-0" - id="pagesize" name="size" type="number" - th:placeholder="${requests.size} + ' requests'" - required/> - - <button type="submit" value="Submit" - class="btn btn-success col-sm-auto">Submit - </button> - </div> - </form> -</section> - -<section layout:fragment="outside-content"> - <div class="row no-gutters"> - <div class="col-lg-3 col-xl-2 pl-lg-3 pr-lg-1"> - <th:block th:replace="request/list/filters :: filters (returnPath='/requests')"> - </th:block> - </div> - <div class="col-lg-9 col-xl-10 pl-lg-1 pr-lg-3"> - <th:block th:replace="request/list/request-table :: request-table"> - </th:block> - </div> + <div class="row col-12"> + <th:block th:replace="request/list/request-table :: request-table(showName=${false}, showOnlyRelevant=${true})"> + </th:block> </div> - <div class="justify-content-center"> + <div class="row col-12"> <th:block th:replace="pagination :: pagination (page=${requests}, size=3)"> </th:block> </div> diff --git a/src/main/resources/templates/request/list/filters.html b/src/main/resources/templates/request/list/filters.html index bcacc158b45ddf5153d2bd61e0b569611bcdfaf5..338acde1303a7332a6bef5220c143ca88385b5f4 100644 --- a/src/main/resources/templates/request/list/filters.html +++ b/src/main/resources/templates/request/list/filters.html @@ -35,135 +35,152 @@ <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script> <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css" type="text/css"/> - <div class="card mb-2 mb-lg-3"> - <div class="card-header" style="background-color: #e9ecef"> - <a class="btn btn-block" type="button" data-toggle="collapse" - data-target="#filter-body"> - <h5 class="pb-0 mb-0">Filters - <span class="fa-pull-right"><i class="fa fa-bars"></i></span> - <span class="badge badge-pill active-filter" - th:if="${filter.countActiveFilters() > 0}" - th:text="${filter.countActiveFilters()}">3</span> - </h5> - </a> - </div> - <div class="collapse card-body" id="filter-body"> - <form id="filter-form" class="form" method="post" th:action="@{/filter}" th:object="${filter}"> - <input type="hidden" name="return-path" th:value="${returnPath}"/> - -<!-- <div class="form-group">--> -<!-- <label for="filter-course">Course</label>--> -<!-- <select multiple class="form-control selectpicker" id="filter-course"--> -<!-- th:field="*{editions}">--> -<!-- <th:block th:each="edition : ${editions}">--> -<!-- <!–@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"–>--> -<!-- <option th:value="${edition.id}"--> -<!-- th:selected="${filter.editions.contains(edition.id)}"--> -<!-- th:text="${edition.name}">--> -<!-- </option>--> -<!-- </th:block>--> -<!-- </select>--> -<!-- </div>--> - - <div class="form-group"> - <label class="form-control-label" for="lab-select">Lab</label> - <select multiple class="form-control selectpicker" 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> - </div> - - <div class="form-group"> - <label class="form-control-label" for="assignment-select">Assignment</label> - <select multiple class="form-control selectpicker" id="assignment-select" - th:field="*{assignments}"> - <th:block th:each="assignment : ${assignments}"> - <option th:value="${assignment.id}" - th:selected="${filter.assignments.contains(assignment.id)}" - th:text="|${assignmentsWithCourseCodes.getOrDefault(assignment.id,'CSEXXXX')} - ${assignment.name}|"> - </option> - </th:block> - </select> - </div> - - <div class="form-group"> - <label class="form-control-label" for="room-select">Room</label> - <select multiple class="form-control selectpicker" id="room-select" th:field="*{rooms}" - data-actions-box="true" data-live-search="true"> - <th:block th:each="room : ${rooms}"> - <option th:value="${room.id}" - th:selected="${filter.rooms.contains(room.id)}" - th:text="|${room.building.name} - ${room.name}|"> - </option> - </th:block> - </select> - </div> - - <div class="form-group"> - <label class="form-control-label" for="onlineMode-select">Online Mode</label> - <select multiple class="form-control selectpicker" id="onlineMode-select" th:field="*{onlineModes}"> - <th:block th:each="onlineMode : ${T(nl.tudelft.queue.model.enums.OnlineMode).values()}"> - <option th:value="${onlineMode}" - th:selected="${filter.onlineModes.contains(onlineMode)}" - th:text="|${onlineMode.displayName}|"> - </option> - </th:block> - </select> - </div> - - <div class="form-group"> - <label class="form-control-label" for="assigned-select">Assigned</label> - <select multiple class="form-control selectpicker" id="assigned-select" - th:field="*{assigned}"> - <th:block th:each="assistant : ${assistants}"> - <option th:value="${assistant.id}" - th:selected="${filter.assigned.contains(assistant.id)}" - th:text="${assistant.displayName}"> - </option> - </th:block> - </select> - </div> +<!-- <div class="card-header" style="background-color: #e9ecef">--> +<!-- <a class="btn btn-block" type="button" data-toggle="collapse"--> +<!-- data-target="#filter-body">--> +<!-- <h5 class="pb-0 mb-0">Filters--> +<!-- <span class="fa-pull-right"><i class="fa fa-bars"></i></span>--> +<!-- <span class="badge badge-pill active-filter"--> +<!-- th:if="${filter.countActiveFilters() > 0}"--> +<!-- th:text="${filter.countActiveFilters()}">3</span>--> +<!-- </h5>--> +<!-- </a>--> +<!-- </div>--> + + <div class="row mb-2 col-12"> + <button type="button" class="btn btn-outline-primary" data-toggle="modal" data-target="#filter-modal"> + <span class="fa fa-filter"></span> + <span>Filters</span> + <span th:if="${filter.countActiveFilters() > 0}" th:text="|(${filter.countActiveFilters()})|"></span> + </button> + </div> - <div class="form-group"> - <label class="form-control-label" for="status-select">Status</label> - <select multiple class="form-control selectpicker" id="status-select" - th:field="*{requestStatuses}"> - <th:block - th:each="status : ${T(nl.tudelft.queue.model.enums.RequestStatus).values()}"> - <option th:value="${status.name()}" - th:selected="${filter.requestStatuses.contains(status)}" - th:text="${status.displayName}"> - </option> - </th:block> - </select> + <div class="modal" id="filter-modal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog modal-dialog-centered"> + <form class="modal-content" method="post" th:action="@{/filter}" th:object="${filter}"> + <div class="modal-header"> + <h3 class="modal-title">Filters</h3> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> </div> - - <div class="form-group"> - <label class="form-control-label" for="request-type-select">Type</label> - <select multiple class="form-control selectpicker" - id="request-type-select" th:field="*{requestTypes}"> - <th:block - th:each="requestType : ${T(nl.tudelft.queue.model.enums.RequestType).values()}"> - <option th:value="${requestType.name()}" - th:selected="${filter.requestTypes.contains(requestType)}" - th:text="${requestType.name()}"> - </option> - </th:block> - </select> + <div class="modal-body"> + <div id="filter-form" class="form"> + <input type="hidden" name="return-path" th:value="${returnPath}"/> + + <!-- <div class="form-group">--> + <!-- <label for="filter-course">Course</label>--> + <!-- <select multiple class="form-control selectpicker" id="filter-course"--> + <!-- th:field="*{editions}">--> + <!-- <th:block th:each="edition : ${editions}">--> + <!-- <!–@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"–>--> + <!-- <option th:value="${edition.id}"--> + <!-- th:selected="${filter.editions.contains(edition.id)}"--> + <!-- th:text="${edition.name}">--> + <!-- </option>--> + <!-- </th:block>--> + <!-- </select>--> + <!-- </div>--> + + <div class="form-group"> + <label class="form-control-label" for="lab-select">Lab</label> + <select multiple class="form-control selectpicker" 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> + </div> + + <div class="form-group"> + <label class="form-control-label" for="assignment-select">Assignment</label> + <select multiple class="form-control selectpicker" id="assignment-select" + th:field="*{assignments}"> + <th:block th:each="assignment : ${assignments}"> + <option th:value="${assignment.id}" + th:selected="${filter.assignments.contains(assignment.id)}" + th:text="|${assignmentsWithCourseCodes.getOrDefault(assignment.id,'CSEXXXX')} - ${assignment.name}|"> + </option> + </th:block> + </select> + </div> + + <div class="form-group"> + <label class="form-control-label" for="room-select">Room</label> + <select multiple class="form-control selectpicker" id="room-select" th:field="*{rooms}" + data-actions-box="true" data-live-search="true"> + <th:block th:each="room : ${rooms}"> + <option th:value="${room.id}" + th:selected="${filter.rooms.contains(room.id)}" + th:text="|${room.building.name} - ${room.name}|"> + </option> + </th:block> + </select> + </div> + + <div class="form-group"> + <label class="form-control-label" for="onlineMode-select">Online Mode</label> + <select multiple class="form-control selectpicker" id="onlineMode-select" th:field="*{onlineModes}"> + <th:block th:each="onlineMode : ${T(nl.tudelft.queue.model.enums.OnlineMode).values()}"> + <option th:value="${onlineMode}" + th:selected="${filter.onlineModes.contains(onlineMode)}" + th:text="|${onlineMode.displayName}|"> + </option> + </th:block> + </select> + </div> + + <div class="form-group"> + <label class="form-control-label" for="assigned-select">Assigned</label> + <select multiple class="form-control selectpicker" id="assigned-select" + th:field="*{assigned}"> + <th:block th:each="assistant : ${assistants}"> + <option th:value="${assistant.id}" + th:selected="${filter.assigned.contains(assistant.id)}" + th:text="${assistant.displayName}"> + </option> + </th:block> + </select> + </div> + + <div class="form-group"> + <label class="form-control-label" for="status-select">Status</label> + <select multiple class="form-control selectpicker" id="status-select" + th:field="*{requestStatuses}"> + <th:block + th:each="status : ${T(nl.tudelft.queue.model.enums.RequestStatus).values()}"> + <option th:value="${status.name()}" + th:selected="${filter.requestStatuses.contains(status)}" + th:text="${status.displayName}"> + </option> + </th:block> + </select> + </div> + + <div class="form-group"> + <label class="form-control-label" for="request-type-select">Type</label> + <select multiple class="form-control selectpicker" + id="request-type-select" th:field="*{requestTypes}"> + <th:block + th:each="requestType : ${T(nl.tudelft.queue.model.enums.RequestType).values()}"> + <option th:value="${requestType.name()}" + th:selected="${filter.requestTypes.contains(requestType)}" + th:text="${requestType.name()}"> + </option> + </th:block> + </select> + </div> + </div> </div> - - <div class="row justify-content-center"> - <button type="submit" class="btn btn-primary col-md-4 mb-1" name="filter-submit"> - Filter - </button> - <button th:unless="${filter.isEmpty()}" - class="btn col-xl-8" name="filter-clear"> + <div class="modal-footer"> + <button th:unless="${filter.isEmpty()}" class="btn" name="filter-clear"> <i class="fa fa-times" aria-hidden="true"></i> Clear current filters </button> + <button type="submit" class="btn btn-primary" name="filter-submit"> + Filter + </button> </div> </form> </div> diff --git a/src/main/resources/templates/request/list/request-table.html b/src/main/resources/templates/request/list/request-table.html index 09b08492383bedfb68db2539c4cd8a4daf1a3246..1d1abd4ce763c4c2307ea2b999b8d2a1a448375d 100644 --- a/src/main/resources/templates/request/list/request-table.html +++ b/src/main/resources/templates/request/list/request-table.html @@ -23,30 +23,30 @@ <!--@thymesVar id="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.RequestViewDTO>"--> <body> -<th:block th:fragment="request-table"> +<th:block th:fragment="request-table(showName, showOnlyRelevant)"> <div class="table-responsive"> <table id="request-table" class="table"> <thead class="thead-light"> <tr class="no-border-top"> <th>Status</th> - <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Type</th> - <th 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:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Assigned</th> - <th th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Handled</th> + <th style="padding-left: .75rem;" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Type</th> + <th th:if="${showName}" style="padding-left: .75rem;" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Requested by</th> + <th style="padding-left: .75rem;" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Room</th> + <th style="padding-left: .75rem;" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Assignment</th> + <th th:styleappend="${showOnlyRelevant} ? '' : 'padding-left: .75rem;'" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-right'">Course</th> + <th th:unless="${showOnlyRelevant}" style="padding-left: .75rem;" 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()}"> + <tr th:if="${requests.isEmpty()}" id="no-requests-info"> <td colspan="8">There aren't any requests.</td> </tr> <!-- Changes made to this template should be made accordingly to the template in request/list.html --> <th:block th:each="request : ${requests}"> <tr class="text-white" th:classappend="${request.eventInfo.status.colourClass}" th:id="'request-' + ${request.id}"> - <td><span class="badge badge-pill bg-info" + <td style="width: 1px;"><span class="badge badge-pill bg-info" th:text="${request.eventInfo.status.displayName}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'" th:id="'status-' + ${request.id}"></span></td> @@ -61,7 +61,7 @@ th:text="${request.requestType.displayName()}"></th:block> </a> </td> - <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> + <td th:if="${showName}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> <a class="text-white" th:href="@{/request/{id}(id=${request.id})}" th:text="${request.requesterEntityName()}"> </a> @@ -83,7 +83,7 @@ <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> <span class="d-inline-block" th:text="${request.organizationName()}"></span> </td> - <td th:id="'assigned-' + ${request.id}" + <td th:unless="${showOnlyRelevant}" th:id="'assigned-' + ${request.id}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> <th:block th:if="${request.eventInfo.assignedTo != null}"> <th:block th:text="${request.eventInfo.assignedTo.displayName}">Name</th:block> @@ -91,7 +91,7 @@ <small th:text="${#temporals.format(request.eventInfo.lastAssignedAt, 'HH:mm')}"></small> </th:block> </td> - <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> + <td th:unless="${showOnlyRelevant}" th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"> <th:block th:if="${request.eventInfo.status.isHandled()}"> <div th:text="${request.eventInfo.assignedTo.displayName}"></div> <small th:text="${#temporals.format(request.eventInfo.handledAt, 'HH:mm')}"></small> @@ -108,7 +108,6 @@ <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</span></td> <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><a th:href="${base + '/{{id}}'}" class="text-white">{{requestTypeDisplayName}}</a></td> - <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><a th:href="${base + '/{{id}}'}" class="text-white">{{requestedBy}}</a></td> {{#if roomName}} {{#if buildingName}} <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><a th:href="${base + '/{{id}}'}" class="text-white">{{buildingName}} - {{roomName}}</a></td> @@ -120,8 +119,6 @@ <small>Right now</small> </td> <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'"><span class="d-inline-block">{{organizationName}}</span></td> - <td th:classappend="${@thymeleafConfig.isTheDay()} ? 'ctrl-alt-right'" id="assigned-{{id}}"></td> - <td></td> </tr> </script> </th:block> 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 fe5d9419c7cb8d28730ce5e75f2bef7cd587eba5..adf90575c06e24e171adbafdf38b5992b8543784 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 @@ -41,7 +41,7 @@ </th:block> <dt>Course</dt> - <dd th:text="${request.edition.course.name}"></dd> + <dd th:text="|${request.edition.course.name} (${request.edition.course.code})|"></dd> <dt>Lab</dt> <dd th:text="${request.session.name}"></dd> diff --git a/src/test/java/nl/tudelft/queue/service/LabServiceTest.java b/src/test/java/nl/tudelft/queue/service/LabServiceTest.java index 2efe779fccbe3a1a0d2f9fd97b04eab55113e471..cace86da5ba351fa30c85f58253113483f9eafc5 100644 --- a/src/test/java/nl/tudelft/queue/service/LabServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/LabServiceTest.java @@ -21,9 +21,11 @@ import static nl.tudelft.queue.misc.QueueSessionStatus.*; import static nl.tudelft.queue.service.LabService.SessionType.REGULAR; import static nl.tudelft.queue.service.LabService.SessionType.SHARED; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -35,6 +37,8 @@ import javax.transaction.Transactional; import nl.tudelft.labracore.api.SessionControllerApi; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.queue.cache.SessionCacheManager; +import nl.tudelft.queue.csv.EmptyCsvException; +import nl.tudelft.queue.csv.InvalidCsvException; import nl.tudelft.queue.dto.create.constraints.ClusterConstraintCreateDTO; import nl.tudelft.queue.dto.create.embeddables.CapacitySessionConfigCreateDTO; import nl.tudelft.queue.dto.create.embeddables.LabRequestConstraintsCreateDTO; @@ -52,10 +56,8 @@ 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.enums.SelectionProcedure; -import nl.tudelft.queue.model.labs.CapacitySession; -import nl.tudelft.queue.model.labs.Lab; -import nl.tudelft.queue.model.labs.RegularLab; -import nl.tudelft.queue.model.labs.SlottedLab; +import nl.tudelft.queue.model.labs.*; +import nl.tudelft.queue.repository.ExamLabRepository; import nl.tudelft.queue.repository.QueueSessionRepository; import org.junit.jupiter.api.BeforeEach; @@ -64,6 +66,8 @@ import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.ContextConfiguration; import org.springframework.ui.Model; @@ -72,6 +76,7 @@ import test.BaseMockConfig; import test.TestDatabaseLoader; import test.labracore.EditionApiMocker; import test.labracore.EditionCollectionApiMocker; +import test.labracore.PersonApiMocker; import test.labracore.SessionApiMocker; import test.test.TestQueueApplication; @@ -106,9 +111,15 @@ class LabServiceTest { @Autowired private SessionApiMocker sApiMocker; + @Autowired + private ExamLabRepository examLabRepository; + @Autowired private TestDatabaseLoader db; + @Autowired + private PersonApiMocker personApiMocker; + private SessionDetailsDTO session1; private SessionDetailsDTO session2; private SessionDetailsDTO session3; @@ -122,6 +133,8 @@ class LabServiceTest { private RegularLab lab1; private SlottedLab lab2; + private ExamLab lab3; + private LabCreateDTO<RegularLab> createDTO; private LabPatchDTO<RegularLab> labPatchDTO; @@ -129,6 +142,8 @@ class LabServiceTest { private Model model; + private RoleDetailsDTO student1; + @BeforeEach void setUp() { model = mock(Model.class); @@ -136,6 +151,7 @@ class LabServiceTest { eApiMocker.mock(); ecApiMocker.mock(); sApiMocker.mock(); + personApiMocker.mock(); session1 = db.getOopNowLcSessionYesterday(); session2 = db.getRlOopSession(); @@ -187,6 +203,9 @@ class LabServiceTest { .build(); addOnlineModesPatchDTO = RegularLabPatchDTO.builder() .onlineModes(Set.of(OnlineMode.JITSI)).build(); + + lab3 = examLabRepository.findAll().get(0); + student1 = db.getOopNowStudents()[0]; } @Test @@ -477,4 +496,27 @@ class LabServiceTest { var result = ls.getOnlineModesInLabSession(testLab.getSession()); assertThat(result).containsExactlyInAnyOrder(OnlineMode.JITSI); } + + @Test + void addStudentToReviewEmptyFile() { + MockMultipartFile file = new MockMultipartFile( + "file", + "test.csv", + MediaType.TEXT_PLAIN_VALUE, + "".getBytes(StandardCharsets.UTF_8)); + assertThatThrownBy(() -> ls.addStudentsToReview(lab3, file)).isInstanceOf(EmptyCsvException.class); + } + + @Test + void addStudentToReviewSuccess() throws EmptyCsvException, InvalidCsvException { + MockMultipartFile file = new MockMultipartFile( + "file", + "test.csv", + MediaType.TEXT_PLAIN_VALUE, + ("name\n" + student1.getPerson().getUsername()).getBytes(StandardCharsets.UTF_8)); + ls.addStudentsToReview(lab3, file); + assertThat(lab3.getPickedStudents()).isNotEmpty(); + assertThat(lab3.getPickedStudents()).containsExactly(student1.getPerson().getId()); + } + }