diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 08f9c0dcbe414bd245fb2ec5fd5b691f72da04f8..88bf543cc9eef3edc2160ae7453a449e49f7f04b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -50,10 +50,10 @@ stages: - build - prepare - test - - review - gitlab reports - publish - deploy + - live checks # Default build cache settings to extend from .build_cached: @@ -76,6 +76,8 @@ stages: gradle_build: extends: .build_cached stage: build + needs: + - gradle_spotless rules: - if: $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "development" || @@ -311,9 +313,6 @@ postgreSQL_test: # Run spotless gradle_spotless: - extends: .build_cached - needs: - - gradle_build rules: - if: $CI_PIPELINE_SOURCE == "trigger" when: never @@ -327,33 +326,13 @@ gradle_spotless: expire_in: 7 days paths: - spotless-diagnose-java/ - stage: review + stage: build script: - gradle spotlessCheck after_script: - cp -r build/spotless-diagnose-java spotless-diagnose-java/ -# Run license check -gradle_licenses: - extends: .build_cached - needs: - - gradle_build - rules: - - if: $CI_PIPELINE_SOURCE == "trigger" - when: never - - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "development" || - $CI_MERGE_REQUEST_ID || - $CI_PIPELINE_SOURCE == "push" - stage: review - script: - - echo "Temp disabled..." - # - gradle licenseMain - # - gradle licenseTest - - - # Publish the JAR for Queue publish_jar: extends: .build_cached @@ -389,6 +368,9 @@ code_quality: # Runs the SAST checks and reporter. spotbugs-sast: + stage: gitlab reports + needs: + - gradle_build variables: COMPILE: "false" allow_failure: true @@ -400,26 +382,35 @@ spotbugs-sast: $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID stage: gitlab reports + +semgrep-sast: + stage: gitlab reports needs: - gradle_build - dependencies: - - gradle_build + variables: + COMPILE: "false" + SECURE_LOG_LEVEL: "debug" + allow_failure: true + rules: + - if: $CI_PIPELINE_SOURCE == "trigger" || + $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" + when: never + - if: $CI_COMMIT_BRANCH == "master" || + $CI_COMMIT_BRANCH == "development" || + $CI_MERGE_REQUEST_ID + stage: gitlab reports # Run the DAST security checks and reporter. # Currently set to manual as it requires a test environment to be up and running. dast: extends: - .build_cached - - .gitlab_reporter - tags: - - longJob rules: - if: $CI_PIPELINE_SOURCE == "trigger" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - - if: $CI_COMMIT_BRANCH == "master" - when: manual - stage: gitlab reports + - if: $CI_COMMIT_BRANCH == "development" + stage: live checks variables: DAST_VERSION: latest @@ -443,8 +434,11 @@ secret_detection: # Dependency scanning reporter for checking dependencies of Queue. dependency_scanning: stage: gitlab reports + extends: + .gitlab_reporter gemnasium-dependency_scanning: + stage: gitlab reports rules: - if: $CI_PIPELINE_SOURCE == "trigger" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" @@ -452,10 +446,6 @@ gemnasium-dependency_scanning: - if: $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID - needs: - - gradle_build - dependencies: - - gradle_build # License scanning reporter for checking the licenses of dependencies. license_scanning: @@ -471,39 +461,31 @@ license_scanning: $CI_MERGE_REQUEST_ID stage: gitlab reports variables: - LM_JAVA_VERSION: 11 + LM_JAVA_VERSION: 17 # Accessibility testing a11y: - extends: - - .gitlab_reporter rules: - if: $CI_PIPELINE_SOURCE == "trigger" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "development" - when: manual - stage: gitlab reports + - if: $CI_COMMIT_BRANCH == "development" + stage: live checks variables: - ally_urls: "https://queue.tudelft.nl" + ally_urls: "https://queue.eiptest.ewi.tudelft.nl" # Accessibility testing browser_performance: - extends: - - .gitlab_reporter rules: - if: $CI_PIPELINE_SOURCE == "trigger" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "development" - when: manual - stage: gitlab reports + - if: $CI_COMMIT_BRANCH == "development" + stage: live checks variables: - URL: "https://queue.tudelft.nl" + URL: "https://queue.eiptest.ewi.tudelft.nl" -# job for deploying on staging +# job for deploying on staging; sleeps for 60 seconds to make sure the live checks can test against running queue deploy_staging: image: getsentry/sentry-cli stage: deploy @@ -520,6 +502,8 @@ deploy_staging: script: - scp queue.jar deployer-queue@eiptest.ewi.tudelft.nl:/var/www/queue/ - ssh deployer-queue@eiptest.ewi.tudelft.nl sudo /bin/systemctl restart queue + after_script: + - sleep 60 environment: name: staging url: https://queue.eiptest.ewi.tudelft.nl @@ -573,7 +557,6 @@ deploy: - if: $CI_PIPELINE_SOURCE == "trigger" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "development" + - if: $CI_COMMIT_BRANCH == "master" when: manual diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e0e7cb134823105cd2ee205b420f6474ec33ae..92373a7b07dc3ea67bbce4898c9f665a95652fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [2.1.1] +### Added + - Added upcoming courses to home page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + +### Changed + - Course codes are now shown next to assignment filter [@hpage](https://gitlab.ewi.tudelft.nl/hpage) + - Moved add edition and add edition collection buttons to home. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + +### Fixed + - Feedback y-axis no longer has labels on increments of 1 [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) +- Request Table Pagination is fixed. [@hpage](https://gitlab.ewi.tudelft.nl/hpage) + - Lab stats page now shows the lab dates [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Request Table Pagination is fixed. [@hpage](https://gitlab.ewi.tudelft.nl/hpage) + - Past labs are now shown on the calendar. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Header and footer no longer stretch across the entire page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Links in the header and footer now behave like links. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Titles of pages are now more consistent. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + - Fixed layout bugs on the catalog page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + +### Deprecated + +### Removed + - Removed breadcrumbs from home page. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx) + ## [2.1.0] ### Added - Redirect users to enrol page when they are not correctly enrolled for a lab. [@cedricwilleken](https://gitlab.ewi.tudelft.nl/cedricwilleken) diff --git a/build.gradle.kts b/build.gradle.kts index 07a19081be111bc28d63f187d14d7855ae814bf4..2a26978c0699f196996252a408bacda1cc892ff3 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.0" +version = "2.1.1" val javaVersion = JavaVersion.VERSION_17 diff --git a/src/main/java/nl/tudelft/queue/controller/EditionController.java b/src/main/java/nl/tudelft/queue/controller/EditionController.java index 215c8011024668c2498bbdc6b98961949562cc12..24bab93a605f1e3bf539820a24e63d2dc7759ee2 100644 --- a/src/main/java/nl/tudelft/queue/controller/EditionController.java +++ b/src/main/java/nl/tudelft/queue/controller/EditionController.java @@ -18,13 +18,10 @@ package nl.tudelft.queue.controller; import static java.time.LocalDateTime.now; -import static nl.tudelft.labracore.api.dto.PersonDetailsDTO.DefaultRoleEnum.ADMIN; -import static nl.tudelft.labracore.api.dto.PersonDetailsDTO.DefaultRoleEnum.TEACHER; import static nl.tudelft.labracore.lib.LabracoreApiUtil.fromPageable; import static nl.tudelft.queue.PageUtil.toPage; import java.io.IOException; -import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -173,22 +170,12 @@ public class EditionController { new PageImpl<>(editions.getContent(), pageable, editions.getTotalElements())); model.addAttribute("programs", cCache.getAll() .stream().map(CourseDetailsDTO::getProgram).distinct() + .sorted(Comparator.comparing(ProgramSummaryDTO::getName)) .collect(Collectors.toList())); model.addAttribute("filter", filter); model.addAttribute("page", "catalog"); - if (pd.getDefaultRole() == ADMIN || pd.getDefaultRole() == TEACHER) { - model.addAttribute("allEditions", eCache.get( - Objects.requireNonNullElse( - eApi.getAllEditionsActiveDuringPeriod( - new Period().start(LocalDateTime.now()) - .end(LocalDateTime.now().plusYears(1))) - .map(EditionSummaryDTO::getId) - .collectList().block(), - List.of()))); - } - return "edition/index"; } diff --git a/src/main/java/nl/tudelft/queue/controller/HomeController.java b/src/main/java/nl/tudelft/queue/controller/HomeController.java index 75fbbea2289192d5699cd94b7fc159a8cb09a386..16cdd161ff245ba5faeb478e9f3dd7c5ffbed088 100644 --- a/src/main/java/nl/tudelft/queue/controller/HomeController.java +++ b/src/main/java/nl/tudelft/queue/controller/HomeController.java @@ -17,6 +17,8 @@ */ package nl.tudelft.queue.controller; +import static nl.tudelft.labracore.api.dto.PersonDetailsDTO.DefaultRoleEnum.ADMIN; +import static nl.tudelft.labracore.api.dto.PersonDetailsDTO.DefaultRoleEnum.TEACHER; import static nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO.TypeEnum.TEACHER_RO; import java.time.LocalDateTime; @@ -158,6 +160,12 @@ public class HomeController { .thenComparing((RoleEditionDetailsDTO r) -> r.getEdition().getEndDate()) .reversed()) .collect(Collectors.toList()); + var upcomingRoles = roles.stream() + .filter(role -> role.getEdition().getStartDate().isAfter(now)) + .sorted(Comparator.comparing(RoleEditionDetailsDTO::getType) + .thenComparing((RoleEditionDetailsDTO r) -> r.getEdition().getStartDate()) + .reversed()) + .collect(Collectors.toList()); var archivedRoles = roles.stream() .filter(role -> role.getEdition().getIsArchived() && Set.of(TEACHER_RO).contains(role.getType())) @@ -167,10 +175,10 @@ public class HomeController { .stream() .collect(Collectors.toMap(EditionDetailsDTO::getId, Function.identity())); - var sessions = sCache.get(editions.values().stream() + Set<SessionDetailsDTO> sessions = new HashSet<>(sCache.get(editions.values().stream() .flatMap(e -> e.getSessions().stream()) - .filter(s -> s.getEnd().isAfter(now)) - .map(SessionSummaryDTO::getId)).stream().collect(Collectors.toSet()); + .filter(s -> s.getEnd().isAfter(now.minusMonths(1))) + .map(SessionSummaryDTO::getId))); var sharedEditions = editions.values().stream().map(EditionDetailsDTO::getEditionCollections) .flatMap(List::stream).map(e -> ecCache.getOrThrow(e.getId())) @@ -180,6 +188,7 @@ public class HomeController { lr.findAllBySessions( sessions.stream().map(SessionDetailsDTO::getId).collect(Collectors.toList()))); + sessions = sessions.stream().filter(s -> s.getEnd().isAfter(now)).collect(Collectors.toSet()); var labs = sessions.stream().collect(Collectors.toMap(SessionDetailsDTO::getId, s -> es.sortLabs(View.convert(lr.findAllBySessions(List.of(s.getId())), QueueSessionSummaryDTO.class)).stream() @@ -197,6 +206,17 @@ public class HomeController { .filter(qs -> ps.canEnqueueSelf(qs.getId()) || qs.getSlot().open()) .collect(Collectors.toList()))); + if (pd.getDefaultRole() == ADMIN || pd.getDefaultRole() == TEACHER) { + model.addAttribute("allEditions", eCache.get( + Objects.requireNonNullElse( + eApi.getAllEditionsActiveDuringPeriod( + new Period().start(LocalDateTime.now()) + .end(LocalDateTime.now().plusYears(1))) + .map(EditionSummaryDTO::getId) + .collectList().block(), + List.of()))); + } + model.addAttribute("editions", editions); model.addAttribute("sharedEditions", sharedEditions); model.addAttribute("sharedLabs", sharedLabs); @@ -205,6 +225,7 @@ public class HomeController { model.addAttribute("activeRoles", activeRoles); model.addAttribute("finishedRoles", finishedRoles); model.addAttribute("archivedRoles", archivedRoles); + model.addAttribute("upcomingRoles", upcomingRoles); model.addAttribute("sessions", calendarEntries); model.addAttribute("page", "my-courses"); diff --git a/src/main/java/nl/tudelft/queue/controller/RequestController.java b/src/main/java/nl/tudelft/queue/controller/RequestController.java index 61489cb9530c2054563d63d8e65a9259355a29e1..fed7995e1da37a7f854fc7bed91c4e175616a8f8 100644 --- a/src/main/java/nl/tudelft/queue/controller/RequestController.java +++ b/src/main/java/nl/tudelft/queue/controller/RequestController.java @@ -46,7 +46,6 @@ import nl.tudelft.queue.repository.LabRequestRepository; import nl.tudelft.queue.service.*; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; @@ -146,15 +145,11 @@ public class RequestController { .collect(Collectors.toList()); List<LabRequest> filteredRequests = rs - .filterRequestsSharedEditionCheck(lrr.findAllByFilter(labs, filter, pageable).getContent(), - assistant); - - var requestsViews = rts.convertRequestsToView( - new PageImpl<>(filteredRequests, pageable, filteredRequests.size()), filteredRequests.size()); + .filterRequestsSharedEditionCheck(lrr.findAllByFilter(labs, filter)); model.addAttribute("page", "requests"); model.addAttribute("filter", filter); - model.addAttribute("requests", requestsViews); + model.addAttribute("requests", rts.convertRequestsToView(filteredRequests, pageable)); model.addAttribute("requestCounts", rts.labRequestCounts( labs, assistant, filter)); diff --git a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java index 35b9e3943acdfd3f90cad1e1d8d482c2fa38ea52..404129d6569fe41d42078d82dee9ea2b0398fac0 100644 --- a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java +++ b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java @@ -330,20 +330,17 @@ public interface LabRequestRepository * requesting to see these requests. The filter contains all labs, rooms, assignments, etc. that need to * be displayed. If a field in the filter is left as an empty set, the filter ignores it. * - * @param labs The labs that the should also be kept in the filter. - * @param filter The filter to apply to the boolean expression. - * @param pageable The pageable object representing the page. - * @return The filtered list of requests. + * @param labs The labs that the should also be kept in the filter. + * @param filter The filter to apply to the boolean expression. + * @return The filtered list of requests. */ - default Page<LabRequest> findAllByFilter(List<Lab> labs, RequestTableFilterDTO filter, - Pageable pageable) { + default List<LabRequest> findAllByFilter(List<Lab> labs, RequestTableFilterDTO filter) { return findAll(qlr.in(select(qlr).from(qlr) .leftJoin(qlr.timeSlot, QTimeSlot.timeSlot).on(qlr.timeSlot.id.eq(QTimeSlot.timeSlot.id)) .where(createFilterBooleanExpression(filter, qlr.session.in(labs).and( qlr.session.type.eq(QueueSessionType.REGULAR) .or(qlr.session.type.in(QueueSessionType.SLOTTED, QueueSessionType.EXAM) - .and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10)))))))), - pageable); + .and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10))))))))); } /** diff --git a/src/main/java/nl/tudelft/queue/service/RequestService.java b/src/main/java/nl/tudelft/queue/service/RequestService.java index b71df2242b7c2952ac932b879d248ebb38efa65f..bff7ffc0e4e9a5531ff7f13ad1614ecf5f232f7f 100644 --- a/src/main/java/nl/tudelft/queue/service/RequestService.java +++ b/src/main/java/nl/tudelft/queue/service/RequestService.java @@ -372,30 +372,34 @@ public class RequestService { * @param filter The filter that the assistant is currently applying to the lab. * @return The request that was picked to handle next or nothing if none could be found. */ - @Transactional(Transactional.TxType.REQUIRES_NEW) public Optional<LabRequest> takeNextRequest(Person assistant, Lab lab, RequestTableFilterDTO filter) { lock.lock(); try { - // If the person is already working on a request, no new event should be created. - Optional<LabRequest> request = lrr.findCurrentlyProcessingRequest(assistant, lab); - if (request.isPresent()) { - return request; - } - - // A new request should be found and assigned to the assistant. - // First comes forwarded requests, then any other type of request. - request = lrr.findCurrentlyForwardedRequest(assistant, lab) - .or(() -> findNextRequest(lab, assistant, filter)); - - request.ifPresent(r -> takeRequest(r, assistant)); - - return request; + return getNextRequest(assistant, lab, filter); } finally { lock.unlock(); } } + @Transactional(Transactional.TxType.REQUIRES_NEW) + protected Optional<LabRequest> getNextRequest(Person assistant, Lab lab, RequestTableFilterDTO filter) { + // If the person is already working on a request, no new event should be created. + Optional<LabRequest> request = lrr.findCurrentlyProcessingRequest(assistant, lab); + if (request.isPresent()) { + return request; + } + + // A new request should be found and assigned to the assistant. + // First comes forwarded requests, then any other type of request. + request = lrr.findCurrentlyForwardedRequest(assistant, lab) + .or(() -> findNextRequest(lab, assistant, filter)); + + request.ifPresent(r -> takeRequest(r, assistant)); + + return request; + } + /** * Assigns a specific request to a user. This however can only be done if the user has no processing * requests open. @@ -521,11 +525,10 @@ public class RequestService { * assistant of. * * @param requests A list of requests to filter - * @param person The assistant that will see the lab requests * @return A new list of lab requests that the assistant will see only if they are an assistant * of the edition the request belongs. */ - public List<LabRequest> filterRequestsSharedEditionCheck(List<LabRequest> requests, Person person) { + public List<LabRequest> filterRequestsSharedEditionCheck(List<LabRequest> requests) { // Check to make sure requests are not empty, otherwise API call fails. if (requests.isEmpty()) { return requests; diff --git a/src/main/java/nl/tudelft/queue/service/RequestTableService.java b/src/main/java/nl/tudelft/queue/service/RequestTableService.java index 29e49ba429f28150cacbc1ee4b3d942500309551..4da5c17e393c9654479fdf11cc3272a3c710f222 100644 --- a/src/main/java/nl/tudelft/queue/service/RequestTableService.java +++ b/src/main/java/nl/tudelft/queue/service/RequestTableService.java @@ -22,9 +22,11 @@ import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.servlet.http.HttpSession; +import nl.tudelft.labracore.api.AssignmentControllerApi; import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.SessionControllerApi; import nl.tudelft.labracore.api.dto.*; @@ -45,6 +47,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.ui.Model; @@ -67,6 +70,8 @@ public class RequestTableService { @Autowired private SessionControllerApi sApi; + @Autowired + private AssignmentControllerApi aApi; @Autowired private AssignmentCacheManager aCache; @@ -194,6 +199,7 @@ public class RequestTableService { model.addAttribute("editions", editions); model.addAttribute("labs", View.convert(labs, QueueSessionSummaryDTO.class)); model.addAttribute("assignments", assignments); + model.addAttribute("assignmentsWithCourseCodes", assignmentsWithCourseCodes(assignments)); model.addAttribute("rooms", rooms); model.addAttribute("assistants", assistants); @@ -233,6 +239,24 @@ public class RequestTableService { requests.getPageable(), total); } + /** + * 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. + */ + public Page<RequestViewDTO<?>> convertRequestsToView(List<? extends Request<?>> requestList, + Pageable pageable) { + var sortedRequestList = requestList.stream() + .sorted(Comparator.comparing((Request<?> r) -> r.getCreatedAt()).reversed()).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, + requestList.size()); + } + /** * Gets the list of all assistants in a list of editions. This list will be necessary for filtering on a * certain assistant within the Queue requests page. @@ -315,4 +339,27 @@ public class RequestTableService { .map(AssignmentSummaryDTO::getId).distinct()); } + /** + * Given a list of assignment details, we create a mapping from the assignment to the corresponding course + * code. + * + * @param assignments The list of assignments to get the edition name for. + * @return The mapping from assignment ids to edition names. + */ + private Map<Long, String> assignmentsWithCourseCodes(List<AssignmentDetailsDTO> assignments) { + List<Long> assignmentIds = assignments.stream().map(AssignmentDetailsDTO::getId).toList(); + + var assignmentModules = Objects + .requireNonNull(aApi.getAssignmentsWithModules(assignmentIds).collectList().block()); + + List<String> courseCodes = eCache + .get(assignmentModules.stream() + .map(assignment -> assignment.getModule().getEdition().getId())) + .stream().map(edition -> edition.getCourse().getCode()).toList(); + + return IntStream.range(0, assignments.size()).boxed() + .collect(Collectors.toMap(assignmentIds::get, courseCodes::get)); + + } + } diff --git a/src/main/resources/scss/_variables.scss b/src/main/resources/scss/_variables.scss index 88f1039c46f483b0174987c8ce9e1e1ea84ebdbd..fe3c3d36fa7568462626c117a9be661745a6a9ff 100644 --- a/src/main/resources/scss/_variables.scss +++ b/src/main/resources/scss/_variables.scss @@ -76,7 +76,7 @@ $not-selected-primary: #af7b2d; $not-selected-secondary: #f0ad4e; :root { - --primary-blue: #00A8DB; + --primary-blue: #00A6D6; --primary-light: #FFFFFF; --primary-dark: #000000; --primary-grey: #CACACA; diff --git a/src/main/resources/templates/edition/index.html b/src/main/resources/templates/edition/index.html index f0a2ccf1b6d82ced6afc2b70ed2b24e58b4539a6..04ed9aa58464f34ac4f64db28e98e7732268b97b 100644 --- a/src/main/resources/templates/edition/index.html +++ b/src/main/resources/templates/edition/index.html @@ -38,50 +38,18 @@ <nav role="navigation" class="breadcrumbs"> <ol class="breadcrumb"> <li class="breadcrumb-item"><a href="/">Home</a></li> - <li class="breadcrumb-item active" aria-current="page">Courses</li> + <li class="breadcrumb-item active" aria-current="page">Catalog</li> </ol> </nav> <div class="page-header"> - <th:block th:if="${@permissionService.isAdminOrTeacher()}"> - <a th:href="@{/editions/request-course}" - th:if="${@mailProperties.enabled}" - class="btn btn-primary float-right ml-1"> - Request Course - </a> - <a th:href="@{/editions/request-course}" - th:unless="${@mailProperties.enabled}" - class="btn btn-primary float-right disabled ml-1" - style="pointer-events: all !important;" - data-toggle="tooltip" data-placement="bottom" - title="The Queue administrator has disabled this feature for now. Please contact them if you need it."> - Request Course - </a> - <a th:href="@{/edition/add}" - class="btn btn-primary float-right" - th:classappend="${!@permissionService.canCreateEdition()} ? 'disabled'" - th:disabled="${!@permissionService.canCreateEdition()}"> - Create new edition - </a> - <button class="btn btn-outline-primary" onclick="showDialog('create-shared-edition-dialog')"> - Create course collection - </button> - <div tabindex="0" class="help-tip"> - <div class="help-icon">?</div> - <p>A course collection is a collection of courses that acts in the queue as one - course. This can be used for sessions in which multiple courses take place at - the same time. This avoids having to create a session for every - course, as sessions can be shared.</p> - </div> - <div th:replace="~{shared-edition/create/create-shared-edition :: overlay}"></div> - </th:block> - <h1>Course Editions</h1> + <h1>Catalog</h1> </div> - <form class="form form-inline" th:action="@{/editions/filter}" method="post"> - <div class="form-group form-row"> - <span class="col-form-label col-sm-4">Programmes: </span> - <div class="col-sm-8"> + <form class="ml-2 mb-3 form-inline" th:action="@{/editions/filter}" method="post"> + <div class="form-row align-items-center mr-2"> + <span class="mr-1">Programmes: </span> + <div> <select multiple class="selectpicker" size="10" id="program-select" name="programs" data-mobile="true" data-width="100%" @@ -96,42 +64,36 @@ </div> </div> - <div class="form-group"> - <input class="form-control" name="nameSearch" th:value="${filter?.nameSearch ?: ''}"> - </div> + <input aria-label="Search for courses" class="form-control mr-2" name="nameSearch" placeholder="Search for courses" th:value="${filter?.nameSearch ?: ''}"> - <div class="form-group"> - <div class="col-sm-2"> - <button class="btn btn-primary" type="submit">Filter</button> - </div> - </div> + <button class="btn btn-primary" type="submit">Filter</button> </form> <!-- Shown on other devices (> phone/tablet) --> <div class="d-none d-sm-block"> - <table th:unless="${#lists.isEmpty(editions)}" class="table table-striped table-bordered"> + <table th:unless="${#lists.isEmpty(editions)}" class="table table-striped "> <thead> <tr> <th>Edition</th> - <th>Code</th> - <th>Teacher</th> + <th style="padding-left: .75rem;">Code</th> + <th style="padding-left: .75rem;">Teachers</th> <th></th> </tr> </thead> <tbody> <tr th:each="edition : ${editions}"> - <td><a th:href="@{/edition/{id}(id=${edition.id})}" + <td><a style="font-size: 11pt;" th:href="@{/edition/{id}(id=${edition.id})}" th:text="|${edition.course.name} (${edition.name})|"></a></td> <td th:text="${edition.course.code}"></td> <td th:text="${#strings.listJoin(@roleDTOService.teacherNames(edition), ', ')}"></td> - <td class="fit"> + <td class="fit text-right"> <th:block th:unless="${@permissionService.canViewEdition(edition) || edition.isArchived }"> <a class="btn btn-success" - th:href="@{/edition/{id}/enrol(id=${edition.id})}">enrol</a> + th:href="@{/edition/{id}/enrol(id=${edition.id})}" style="min-width: 4rem;">enrol</a> </th:block> <th:block th:if="${@permissionService.canViewEdition(edition)}"> <a class="btn btn-primary" - th:href="@{/edition/{id}(id=${edition.id})}">view</a> + th:href="@{/edition/{id}(id=${edition.id})}" style="min-width: 4rem;">view</a> </th:block> </td> </tr> diff --git a/src/main/resources/templates/edition/view/status.html b/src/main/resources/templates/edition/view/status.html index 77053ed6745577fc6e63b67e063d0502c8decf8d..7ef38f835de2987ae1434a0c3e4eb0f19465daee 100644 --- a/src/main/resources/templates/edition/view/status.html +++ b/src/main/resources/templates/edition/view/status.html @@ -84,13 +84,13 @@ <option disabled>Active</option> <option th:each="lab : ${activeLabs}" th:value="${lab.id}" - th:text="${lab.name}" + th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'd MMMM')}|" selected> </option> <option disabled>Old</option> <option th:each="lab : ${inactiveLabs}" th:value="${lab.id}" - th:text="${lab.name}" + th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'd MMMM')}|" selected> </option> </select> @@ -234,13 +234,13 @@ <option disabled>Active</option> <option th:each="lab : ${activeLabs}" th:value="${lab.id}" - th:text="${lab.name}" + th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'd MMMM')}|" selected> </option> <option disabled>Old</option> <option th:each="lab : ${inactiveLabs}" th:value="${lab.id}" - th:text="${lab.name}" + th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'd MMMM')}|" selected> </option> </select> diff --git a/src/main/resources/templates/home/dashboard.html b/src/main/resources/templates/home/dashboard.html index d09211c3b153b4a6999772297c93c5f5d1d4df5b..5a7587764e700f8f5eccb4b4bd3a94f096bb809c 100644 --- a/src/main/resources/templates/home/dashboard.html +++ b/src/main/resources/templates/home/dashboard.html @@ -36,14 +36,33 @@ <body> <section layout:fragment="content"> - <nav role="navigation" class="breadcrumbs"> - <ol class="breadcrumb"> - <li class="breadcrumb-item active" aria-current="page">Home</li> - </ol> - </nav> - <div class="page-header"> - <h1>Home</h1> + + <h1>My Courses</h1> + </div> + + <div class="row ml-0 mb-3"> + <th:block th:if="${@permissionService.isAdminOrTeacher()}"> + <a th:href="@{/edition/add}" + class="btn btn-primary" + th:classappend="${!@permissionService.canCreateEdition()} ? 'disabled'" + th:disabled="${!@permissionService.canCreateEdition()}"> + Create new edition + </a> + <div class="ml-1"> + <button class="btn btn-outline-primary" onclick="showDialog('create-shared-edition-dialog')"> + Create course collection + </button> + <div tabindex="0" class="help-tip"> + <div class="help-icon">?</div> + <p>A course collection is a collection of courses that acts in the queue as one + course. This can be used for sessions in which multiple courses take place at + the same time. This avoids having to create a session for every + course, as sessions can be shared.</p> + </div> + </div> + <div th:replace="~{shared-edition/create/create-shared-edition :: overlay}"></div> + </th:block> </div> <ul class="nav nav-tabs" id="overview-tabs" role="tablist"> @@ -125,7 +144,12 @@ <div class="boxed-group"> <h3>Active courses you participate in</h3> <div class="boxed-group-inner" th:if="${#lists.isEmpty(activeRoles)}"> - You do not participate in any courses. Why don't you <a th:href="@{/editions}">enrol for your first course</a>? + <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> + <span th:if="${user.defaultRole.name() == 'ADMIN' or user.defaultRole.name() == 'TEACHER'}"> + You do not have any active courses. You can create a course edition by clicking 'Create new edition' above. + </span> </div> <ul class="list-group"> @@ -192,6 +216,19 @@ </ul> </div> + <div class="boxed-group" th:if="${!#lists.isEmpty(upcomingRoles)}"> + <h3>Upcoming courses</h3> + + <ul class="list-group"> + <li class="list-group-item" th:each="role : ${upcomingRoles}" + 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> + </li> + </ul> + </div> + <div class="boxed-group" th:if="${!#lists.isEmpty(finishedRoles)}"> <h3>Finished courses</h3> diff --git a/src/main/resources/templates/home/feedback.html b/src/main/resources/templates/home/feedback.html index 14a34db2ce22fa4340b28517f36e78971e2eeb36..b23515212b6dab392ef4f167d2be544a1475a10a 100644 --- a/src/main/resources/templates/home/feedback.html +++ b/src/main/resources/templates/home/feedback.html @@ -119,7 +119,7 @@ ticks: { beginAtZero: true, precision: 0, - stepSize: 1 + callback: (value) => { if (value % 1 === 0) return value; }, } }] } diff --git a/src/main/resources/templates/home/index.html b/src/main/resources/templates/home/index.html index c0786acc19f909ffa73e2cb7998ae441748f63a0..af3afa7d33eb985fbf53f5e13082dab3b69e76e5 100644 --- a/src/main/resources/templates/home/index.html +++ b/src/main/resources/templates/home/index.html @@ -38,10 +38,13 @@ <h2>How to use this system?</h2> - After you login, you can <em>enrol</em> for a particular course. Once enrolled you can navigate to a specific + <p>After you login, you can <em>enrol</em> for a particular course. Once enrolled you can navigate to a specific lab. If the lab is opened, you can <em>enqueue</em> for a specific assignment. The assistants process this queue. You will receive a notification when an assistant is assigned to your request. The notification - instructs you to either visit the TA or wait for the TA to visit you. + instructs you to either visit the TA or wait for the TA to visit you.</p> + + <a th:href="@{/login}" class="btn btn-primary">Click here to log in</a> + </section> </body> </html> diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index 7722dc4fd877670ad6e2cd3b31f33a460ab18a19..c84a36be7cec9d444f53e235e585585fd9caecfe 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -70,7 +70,8 @@ <!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.lib.security.user.Person"--> <body class="d-flex flex-column" style="min-height: 100vh"> -<nav class="navbar navbar-expand-lg navbar-inverse text-white"> +<div class="justify-content-center" style="width: 100%; background-color: var(--primary-blue);"> +<nav class="navbar navbar-expand-lg navbar-inverse text-white mx-auto" style="max-width: 75rem;"> <!-- Navigation bar with courses, requests, admin, login, request history etc. --> <a class="navbar-brand" href="/" th:unless="${@thymeleafConfig.isTheDay()}">Queue</a> <a class="navbar-brand" href="/" th:if="${@thymeleafConfig.isTheDay()}">Stack</a> @@ -121,11 +122,13 @@ </li> </th:block> <li class="nav-item" th:unless="${#authenticatedP != null}"> - <a class="nav-link" th:href="@{/login}">Login</a> + <a class="pr-2" th:href="@{/login}">Login</a> </li> </ul> </div> </nav> +</div> + <main class="flex-fill fluid-container"> <!-- Announcement banners --> @@ -145,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"> + <div class="pl-3 pr-3 col-12 col-sm-11 col-md-10 col-lg-9 col-xl-8" style="max-width: 75rem;"> <th:block layout:fragment="content" class="content"> <p>Page content goes here</p> </th:block> @@ -172,16 +175,16 @@ <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"> + <div class="col-12 col-lg-9 col-xl-8 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> </div> - <div class="col-5 col-sm-2"> - <a class="text-muted btn" th:href="@{/privacy}">Privacy</a> + <div class="col-5 col-sm-2" style="display: flex; align-items: center; justify-content: flex-start"> + <a class="text-muted" th:href="@{/privacy}">Privacy</a> </div> - <div class="col-4 col-sm-2"> - <a class="text-muted btn" th:href="@{/about}">About</a> + <div class="col-4 col-sm-2" style="display: flex; align-items: center; justify-content: flex-start"> + <a class="text-muted" th:href="@{/about}">About</a> </div> <div class="col-3 col-sm-2"> <img th:src="@{${@instituteProperties.logo}}" diff --git a/src/main/resources/templates/request/list/filters.html b/src/main/resources/templates/request/list/filters.html index 6a16cb88320760d9dd3afd8811a00c6c36e57ba3..bcacc158b45ddf5153d2bd61e0b569611bcdfaf5 100644 --- a/src/main/resources/templates/request/list/filters.html +++ b/src/main/resources/templates/request/list/filters.html @@ -22,9 +22,10 @@ <!--@thymesVar id="editions" type="java.util.List<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"--> <!--@thymesVar id="labs" type="java.util.List<nl.tudelft.queue.dto.view.QueueSessionSummaryDTO>"--> -<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"--> +<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentDetailsDTO>"--> <!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"--> <!--@thymesVar id="assistants" type="java.util.List<nl.tudelft.labracore.api.dto.PersonSummaryDTO>"--> +<!--@thymesVar id="assignmentsWithCourseCodes" type="java.util.Map<java.lang.Long,java.lang.String>"--> <!--@thymesVar id="filter" type="nl.tudelft.queue.dto.util.RequestTableFilterDTO"--> <!--@thymesVar id="returnPath" type="java.lang.String"--> @@ -83,7 +84,7 @@ <th:block th:each="assignment : ${assignments}"> <option th:value="${assignment.id}" th:selected="${filter.assignments.contains(assignment.id)}" - th:text="${assignment.name}"> + th:text="|${assignmentsWithCourseCodes.getOrDefault(assignment.id,'CSEXXXX')} - ${assignment.name}|"> </option> </th:block> </select> diff --git a/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java b/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java index ef4a3b56b8acc29ce41d945faef092d2ff934174..a5598ca589e939e7b3a36a743bfa053ab8ce755d 100644 --- a/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java +++ b/src/test/java/nl/tudelft/queue/controller/EditionControllerTest.java @@ -136,8 +136,7 @@ public class EditionControllerTest { mvc.perform(get("/editions")) .andExpect(status().isOk()) .andExpect(model().attribute("page", "catalog")) - .andExpect(model().attributeExists("editions")) - .andExpect(model().attributeExists("allEditions")); + .andExpect(model().attributeExists("editions")); } @Test diff --git a/src/test/java/nl/tudelft/queue/controller/LabRequestControllerTest.java b/src/test/java/nl/tudelft/queue/controller/LabRequestControllerTest.java index 165a508b0a3d12cb2661d3bdf2a02295a6c5595f..30535a633c6561eb529c2ca7901f775d41ef1478 100644 --- a/src/test/java/nl/tudelft/queue/controller/LabRequestControllerTest.java +++ b/src/test/java/nl/tudelft/queue/controller/LabRequestControllerTest.java @@ -190,7 +190,8 @@ class LabRequestControllerTest { mvc.perform(get("/requests")) .andExpect(status().isOk()) .andExpect(model().attribute("page", "requests")) - .andExpect(model().attribute("filter", new RequestTableFilterDTO())); + .andExpect(model().attribute("filter", new RequestTableFilterDTO())) + .andExpect(model().attributeExists("assignmentsWithCourseCodes")); verify(permissionService, atLeastOnce()).canViewRequests(); verify(requestTableService).checkAndStoreFilterDTO(null, "/requests"); diff --git a/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java b/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java index 597f8f8c5670829f720f16e85c60b45bf228141f..fa740fd4c2dccd61a449475b550ea2709e7285be 100644 --- a/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java @@ -33,7 +33,6 @@ import nl.tudelft.labracore.api.AssignmentControllerApi; import nl.tudelft.labracore.api.RoleControllerApi; import nl.tudelft.labracore.api.StudentGroupControllerApi; import nl.tudelft.labracore.api.dto.*; -import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.dto.create.requests.SelectionRequestCreateDTO; import nl.tudelft.queue.model.LabRequest; @@ -343,7 +342,7 @@ public class RequestServiceTest { @Test void sharedEditionFilterDoesNotCallApiWhenEmptyRequests() { - assertThat(rs.filterRequestsSharedEditionCheck(new ArrayList<>(), new Person())).isEmpty(); + assertThat(rs.filterRequestsSharedEditionCheck(new ArrayList<>())).isEmpty(); verify(asApi, never()).getAssignmentsWithModules(any()); @@ -352,9 +351,7 @@ public class RequestServiceTest { @Test @WithUserDetails("student200") void oopTaGetsOopRequestsOnlyInSharedSession() { - Person p1 = new Person(); - p1.setId(oopNowTAs[0].getPerson().getId()); - assertThat(rs.filterRequestsSharedEditionCheck(Arrays.stream(rlOopNowSharedLabRequests).toList(), p1)) + assertThat(rs.filterRequestsSharedEditionCheck(Arrays.stream(rlOopNowSharedLabRequests).toList())) .containsExactly(rlOopNowSharedLabRequests); }