Skip to content
Snippets Groups Projects
Commit 40c39607 authored by Henry Page's avatar Henry Page :speech_balloon:
Browse files

change anchor to button for leave edition dialog prompt

parents 5067d816 6c3736ea
Branches
Tags
2 merge requests!735Resolve "Enhance Workflow of leaving editions",!724Deploy
Showing
with 577 additions and 94 deletions
......@@ -12,13 +12,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- A lab statistics page can now be accessed from the main lab page. This is currently a work in progress. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
- A table showing request per ta and time since last interaction was added in the lab statistics page. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
- A few general statistics was added in the lab statistics page. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
- A bar-graph detailing the assignment breakdown between questions and submissions was added in the lab statistics page. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
### Changed
### Fixed
### Changed
- Written feedback sorting now follows a reverse chronological order. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
- Users will see a dialog instead of a new page when promopted to unenrol from an edition. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
- Text used to indicate the number of times a lab is rescheduled is now more intuitive. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
### Fixed
- The request history page can now be seen for participants. [@mmadara](https://gitlab.ewi.tudelft.nl/mmadara)
- Teachers can now view their own feedback again. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
- The feedback graph for assistants now incorporates feedback from other courses, with written feedback limited to visibility for course managers. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
- The button that lets users unenrol from an edition now disabled when appropriate. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
......@@ -31,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2.2.2]
### Added
- A table showing request per ta and time since last interaction was added in the lab statistics page. [@hpage](https://gitlab.ewi.tudelft.nl/hpage)
### Changed
......
......@@ -17,12 +17,12 @@
*/
package nl.tudelft.queue.controller;
import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*;
import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.TA;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
......@@ -30,16 +30,19 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import nl.tudelft.labracore.api.dto.EditionSummaryDTO;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.labracore.api.dto.RolePersonDetailsDTO;
import nl.tudelft.librador.resolver.annotations.PathEntity;
import nl.tudelft.queue.cache.AssignmentCacheManager;
import nl.tudelft.queue.cache.EditionRolesCacheManager;
import nl.tudelft.queue.dto.view.statistics.AssistantSessionStatisticsViewDto;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.QueueSession;
import nl.tudelft.queue.cache.ModuleCacheManager;
import nl.tudelft.queue.dto.view.statistics.session.AssignmentSessionCountViewDto;
import nl.tudelft.queue.dto.view.statistics.session.AssistantSessionStatisticsViewDto;
import nl.tudelft.queue.dto.view.statistics.session.GeneralSessionStatisticsViewDto;
import nl.tudelft.queue.model.embeddables.AllowedRequest;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.labs.Lab;
import nl.tudelft.queue.service.EditionStatusService;
import nl.tudelft.queue.service.RequestService;
import nl.tudelft.queue.service.RoleDTOService;
import nl.tudelft.queue.service.SessionStatusService;
@RestController
......@@ -57,39 +60,82 @@ public class SessionStatusController {
@Autowired
private EditionStatusService editionStatusService;
@Autowired
private RoleDTOService roleDTOService;
@Autowired
private AssignmentCacheManager aCache;
@Autowired
private ModuleCacheManager mCache;
/**
* Endpoint for general information displayed at the top of the lab statistics page.
*
* @param qSession The session to consider
* @param editions The editions to consider (useful for shared sessions)
* @return A DTO containing the aforementioned information
*/
@GetMapping("/lab/{qSession}/status/general")
@PreAuthorize("@permissionService.canManageSession(#qSession)")
public GeneralSessionStatisticsViewDto generalStatistics(@PathEntity Lab qSession,
@RequestParam(required = false, defaultValue = "") Set<Long> editions) {
var requests = requestService.getLabRequestsForEditions(qSession.getRequests(), editions);
return new GeneralSessionStatisticsViewDto(
(long) requests.size(),
requests.stream().filter(rq -> rq.getRequestType() == RequestType.QUESTION).count(),
requests.stream().filter(rq -> rq.getRequestType() == RequestType.SUBMISSION).count(),
(long) qSession.getQueue().size(),
editionStatusService.averageWaitingTime(requests),
editionStatusService.averageProcessingTime(requests));
}
/**
* Gets a DTO containing information about assistants, how many requests they have taken and the time
* since their last request interaction.
*
* @param qSession The session to consider
* @param editions The editions to consider (useful for shared labs)
* @param editions The editions to consider (useful for shared sessions)
* @return A DTO containing the relevant information
*/
@GetMapping("/lab/{qSession}/status/assistant/freq")
@PreAuthorize("@permissionService.canManageSession(#qSession)")
public AssistantSessionStatisticsViewDto getRequestFrequency(@PathEntity QueueSession<?> qSession,
public AssistantSessionStatisticsViewDto getRequestFrequency(@PathEntity Lab qSession,
@RequestParam(required = false, defaultValue = "") Set<Long> editions) {
Map<Long, String> assistantNames = eRolesCache
.getAndIgnoreMissing(
qSession.getSessionDto().getEditions().stream().map(EditionSummaryDTO::getId)
.filter(eId -> editions.isEmpty() || editions.contains(eId)))
.stream()
.flatMap(e -> e.getRoles().stream())
.filter(r -> Set.of(TEACHER, TEACHER_RO, HEAD_TA, TA).contains(r.getType()))
.map(RolePersonDetailsDTO::getPerson)
.distinct()
.collect(Collectors.toMap(PersonSummaryDTO::getId, PersonSummaryDTO::getDisplayName));
var filteredRequests = qSession.getRequests().stream()
.filter(request -> editions.isEmpty() || editions
.contains(requestService.getEditionForLabRequest((LabRequest) request).getId()))
.map(request -> ((LabRequest) request))
.toList();
return new AssistantSessionStatisticsViewDto(assistantNames,
editionStatusService.countRequestsPerAssistant(filteredRequests),
Map<Long, String> staffNames = roleDTOService.staffNames(qSession, editions);
var requests = requestService.getLabRequestsForEditions(qSession.getRequests(), editions);
return new AssistantSessionStatisticsViewDto(staffNames,
editionStatusService.countRequestsPerAssistant(requests),
sessionStatusService.getTimeSinceLastInteraction(qSession, editions));
}
@GetMapping("/lab/{qSession}/status/assignment/freq")
@PreAuthorize("@permissionService.canManageSession(#qSession)")
@Transactional
public List<AssignmentSessionCountViewDto> getAssignmentFrequency(@PathEntity Lab qSession,
@RequestParam(required = false, defaultValue = "") Set<Long> editions) {
var requests = requestService.getLabRequestsForEditions(qSession.getRequests(), editions);
var assignments = aCache
.getAndHandle(
qSession.getAllowedRequests().stream().map(AllowedRequest::getAssignment).distinct(),
id -> qSession.getAllowedRequests()
.removeIf(a -> Objects.equals(id, a.getAssignment())))
.stream()
.filter(a -> editions.isEmpty() || editions.contains(mCache
.getRequired(a.getModule().getId(), id -> qSession.getModules().remove(id))
.getEdition().getId()))
.toList();
return sessionStatusService.countAssignmentFreqs(requests, assignments);
}
}
/*
* Queue - A Queueing system that can be used to handle labs in higher education
* Copyright (C) 2016-2021 Delft University of Technology
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package nl.tudelft.queue.dto.view.statistics.session;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AssignmentSessionCountViewDto {
private Long assignmentId;
private String assignmentName;
private Long questionCount;
private Long submissionCount;
}
/*
* Queue - A Queueing system that can be used to handle labs in higher education
* Copyright (C) 2016-2021 Delft University of Technology
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package nl.tudelft.queue.dto.view.statistics.session;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AssistantSessionStatisticsViewDto {
private Map<Long, String> assistantNames;
private Map<String, Long> requestsPerAssistant;
private Map<Long, Long> timeSinceLastRequestInteraction; // only includes TAs that have interactions!
}
/*
* Queue - A Queueing system that can be used to handle labs in higher education
* Copyright (C) 2016-2021 Delft University of Technology
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package nl.tudelft.queue.dto.view.statistics.session;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GeneralSessionStatisticsViewDto {
private Long numRequests;
private Long numQuestions;
private Long numSubmissions;
private Long numEnqueued;
private Long avgWaitingTime;
private Long avgProcessingTime;
}
......@@ -40,6 +40,7 @@ import nl.tudelft.queue.cache.RoomCacheManager;
import nl.tudelft.queue.cache.SessionCacheManager;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.QLabRequest;
import nl.tudelft.queue.model.Request;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.labs.Lab;
import nl.tudelft.queue.repository.LabRequestRepository;
......@@ -265,7 +266,7 @@ public class EditionStatusService {
*/
public Long countDistinctUsers(List<LabRequest> requests) {
return requests.stream()
.map(r -> r.getStudentGroup())
.map(Request::getStudentGroup)
.distinct()
.count();
}
......
......@@ -545,7 +545,7 @@ public class RequestService {
* of the edition the request belongs.
*/
public List<LabRequest> filterRequestsSharedEditionCheck(List<LabRequest> requests) {
// Check to make sure requests are not empty, otherwise API call fails.
if (requests.isEmpty()) {
return requests;
}
......@@ -618,14 +618,31 @@ public class RequestService {
}
}
/**
* Filters for lab requests that have assignments belonging to specific editions.
*
* @param labRequests The list of lab requests to consider.
* @param editions The set of editions to filter on.
* @return A sublist of the original list which only includes requests that have corresponding
* editions.
*/
public List<LabRequest> getLabRequestsForEditions(List<LabRequest> labRequests, Set<Long> editions) {
return labRequests
.stream()
.filter(request -> editions.isEmpty()
|| editions.contains(getEditionForLabRequest(request).getId()))
.toList();
}
/**
* Gets the associated edition of a given lab request.
*
* @param request The lab request in question
* @return The edition that this lab request corresponds to.
*/
public EditionSummaryDTO getEditionForLabRequest(LabRequest request) {
private EditionSummaryDTO getEditionForLabRequest(LabRequest request) {
return mCache.getRequired(aCache.getRequired(request.getAssignment()).getModule().getId())
.getEdition();
}
}
......@@ -20,6 +20,7 @@ package nl.tudelft.queue.service;
import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
......@@ -32,6 +33,7 @@ import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.queue.cache.EditionCacheManager;
import nl.tudelft.queue.cache.EditionRolesCacheManager;
import nl.tudelft.queue.model.QueueSession;
@Service
@AllArgsConstructor
......@@ -47,6 +49,26 @@ public class RoleDTOService {
return people.stream().map(PersonSummaryDTO::getDisplayName).collect(Collectors.toList());
}
/**
* Gets the names of staff in a lab.
*
* @param qSession The session to consider.
* @param editions Filter by editions, useful for shared labs, leave empty for no filter.
* @return A mapping of ids to names of staff
*/
public Map<Long, String> staffNames(QueueSession<?> qSession, Set<Long> editions) {
return erCache
.getAndIgnoreMissing(
qSession.getSessionDto().getEditions().stream().map(EditionSummaryDTO::getId)
.filter(eId -> editions.isEmpty() || editions.contains(eId)))
.stream()
.flatMap(e -> e.getRoles().stream())
.filter(this::isStaff)
.map(RolePersonDetailsDTO::getPerson)
.distinct()
.collect(Collectors.toMap(PersonSummaryDTO::getId, PersonSummaryDTO::getDisplayName));
}
public List<String> roleNames(List<RolePersonDetailsDTO> roles,
Set<RolePersonDetailsDTO.TypeEnum> types) {
return roles.stream()
......@@ -91,23 +113,16 @@ public class RoleDTOService {
* @return The user-friendly representation of the role type.
*/
public String typeDisplayName(String type) {
switch (type) {
case "STUDENT":
return "Student";
case "TA":
return "TA";
case "HEAD_TA":
return "Head TA";
case "TEACHER":
return "Teacher";
case "TEACHER_RO":
return "Read-Only Teacher";
case "ADMIN":
return "Admin";
case "BLOCKED":
return "Blocked";
}
return "No role";
return switch (type) {
case "STUDENT" -> "Student";
case "TA" -> "TA";
case "HEAD_TA" -> "Head TA";
case "TEACHER" -> "Teacher";
case "TEACHER_RO" -> "Read-Only Teacher";
case "ADMIN" -> "Admin";
case "BLOCKED" -> "Blocked";
default -> "No role";
};
}
public List<String> assistantNames(EditionDetailsDTO eDto) {
......
......@@ -25,10 +25,13 @@ import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import nl.tudelft.labracore.api.dto.AssignmentDetailsDTO;
import nl.tudelft.queue.dto.view.statistics.session.AssignmentSessionCountViewDto;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.QueueSession;
import nl.tudelft.queue.model.RequestEvent;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.events.EventWithAssistant;
import nl.tudelft.queue.model.labs.Lab;
@Service
public class SessionStatusService {
......@@ -45,10 +48,10 @@ public class SessionStatusService {
* @return A mapping of (already participating) assistant IDs to the time in milliseconds since
* their last request interaction.
*/
public Map<Long, Long> getTimeSinceLastInteraction(QueueSession<?> qs, Set<Long> editions) {
return qs.getRequests().stream()
.filter(request -> editions.isEmpty() || editions
.contains(requestService.getEditionForLabRequest(((LabRequest) request)).getId()))
public Map<Long, Long> getTimeSinceLastInteraction(Lab qs, Set<Long> editions) {
return requestService.getLabRequestsForEditions(qs.getRequests(), editions)
.stream()
.flatMap(request -> request.getEventInfo().getEvents().stream())
.filter(event -> event instanceof EventWithAssistant)
.map(event -> (EventWithAssistant) event)
......@@ -60,4 +63,31 @@ public class SessionStatusService {
}
/**
* Maps the assignments and counts the question and submission frequency of each assignments among a
* collection of requests.
*
* @param requests The requests to consider
* @param assignments The assignments you want to count the question and submission frequencies of
* @return A list of DTOs which capture this information for each assignment
*/
public List<AssignmentSessionCountViewDto> countAssignmentFreqs(List<LabRequest> requests,
List<AssignmentDetailsDTO> assignments) {
return assignments.stream()
.sorted(Comparator.comparing(AssignmentDetailsDTO::getId))
.map(assignment -> {
var assignmentName = assignment.getName();
var assignmentQuestionCount = requests.stream()
.filter(rq -> Objects.equals(rq.getAssignment(), assignment.getId())
&& rq.getRequestType() == RequestType.QUESTION)
.count();
var assignmentSubmissionCount = requests.stream()
.filter(rq -> Objects.equals(rq.getAssignment(), assignment.getId())
&& rq.getRequestType() == RequestType.SUBMISSION)
.count();
return new AssignmentSessionCountViewDto(assignment.getId(), assignmentName,
assignmentQuestionCount, assignmentSubmissionCount);
}).toList();
}
}
let assignmentFreqChart;
/**
* Deletes all previous data and refreshes the table based on new data.
* The latest request interactions are shown at the top of the table.
......@@ -27,6 +29,88 @@ function updateAssistantFrequencyTable(table) {
};
}
/**
* Updates the assigment frequency chart with new incoming data, creates chart if it doesn't exist.
*
* @param canvas The canvas element to create/edit the chart on
* @returns {(function(*))|*} A functinon that updates/creates the chart based on given data.
*/
function updateAssignmentFrequencyChart(canvas) {
return data => {
const assignmentNames = data.map(assignment => assignment["assignmentName"]);
const assignmentSubmissionCounts = data.map(assignment => assignment["submissionCount"]);
const assignmentQuestionCounts = data.map(assignment => assignment["questionCount"]);
if (assignmentFreqChart) {
assignmentFreqChart.data.labels = assignmentNames;
assignmentFreqChart.data.datasets[0].data = assignmentSubmissionCounts;
assignmentFreqChart.data.datasets[1].data = assignmentQuestionCounts;
assignmentFreqChart.update();
} else {
assignmentFreqChart = new Chart(canvas, {
type: "bar",
data: {
labels: assignmentNames,
datasets: [
{
label: "Submission",
backgroundColor: "#c79025",
data: assignmentSubmissionCounts,
},
{
label: "Question",
backgroundColor: "#074980",
data: assignmentQuestionCounts,
},
],
},
options: {
plugins: {
legend: {
position: "bottom",
},
},
scales: {
x: {
stacked: true,
ticks: {
autoSkip: false,
},
},
y: {
stacked: true,
ticks: {
min: 0,
stepSize: 1,
beginAtZero: true,
},
},
},
},
});
}
};
}
/**
* Updates the general information inside the statistic cards
* @param infoCards The infoCards div containing all the stat cards
* @returns {(function(*))|*} A function that will take the data and update the cards.
*/
function updateGeneralInformation(infoCards) {
return data => {
$(infoCards).find("#card-request-count").html(data["numRequests"]);
$(infoCards).find("#card-submission-count").html(data["numSubmissions"]);
$(infoCards).find("#card-question-count").html(data["numQuestions"]);
$(infoCards).find("#card-enqueued-count").html(data["numEnqueued"]);
$(infoCards)
.find("#card-waiting-time")
.html(msToHumanReadableTime(parseInt(data["avgWaitingTime"]) * 1000));
$(infoCards)
.find("#card-processing-time")
.html(msToHumanReadableTime(parseInt(data["avgProcessingTime"]) * 1000));
};
}
function msToHumanReadableTime(ms) {
let seconds = (ms / 1000).toFixed(1);
let minutes = (ms / (1000 * 60)).toFixed(1);
......
......@@ -76,19 +76,15 @@
<a th:href="@{/edition/{id}/enrol(id=${edition.id})}" th:if="${@permissionService.canEnrolForEdition(edition.id)}" class="button">
Enrol for this edition
</a>
<a
th:if="${@permissionService.canViewEdition(edition.id)} and ${@permissionService.canLeaveEdition(edition.id)}"
<button
th:if="${@permissionService.canViewEdition(edition.id)}"
class="button"
data-style="outlined"
data-type="error"
th:attrappend="data-style=${@permissionService.canLeaveEdition(edition.id)}?outlined"
th:disabled="not ${@permissionService.canLeaveEdition(edition.id)}"
th:data-dialog="leave-edition-dialog">
Leave edition
</a>
<button
class="button"
th:if="${@permissionService.canViewEdition(edition.id)} and not ${@permissionService.canLeaveEdition(edition.id)}"
disabled>
Leave edition
</button>
</div>
</div>
......
......@@ -18,22 +18,24 @@
-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{container}">
<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
<!--@thymesVar id="student" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"-->
<!--@thymesVar id="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.model.LabRequest>"-->
<head>
<link rel="stylesheet" href="/css/request_colours.css" />
</head>
<body>
<section layout:fragment="content">
<nav role="navigation" class="breadcrumbs">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item">History</li>
</ol>
<nav role="navigation" class="breadcrumbs mb-5">
<a th:href="@{/}">Home</a>
<span>&gt;</span>
<a th:href="@{/history/edition/{editionId}/student/{studentId}(editionId=${edition.id}, studentId=${student.id})}">History</a>
</nav>
<div class="page-header">
<h1>
<h1 class="font-800 mb-5">
Requests of
<span th:text="${student.displayName}"></span>
</h1>
......@@ -41,9 +43,7 @@
<th:block
th:replace="request/list/filters :: filters (returnPath=@{/history/course/{editionId}/student/{studentId}(editionId=${edition.id}, studentId=${student.id})}, multipleLabs=${true})"></th:block>
</section>
<section layout:fragment="outside-content">
<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>
......
......@@ -100,7 +100,7 @@
<div id="repeat-tab" role="tabpanel" hidden>
<div class="grid col-2 align-center gap-3" style="--col-1: minmax(0, 10rem)">
<label for="week-repeat-input">Repeat for x weeks:</label>
<label for="week-repeat-input">Schedule for x Additional Weeks:</label>
<div>
<input
id="week-repeat-input"
......@@ -108,7 +108,14 @@
type="number"
class="textfield"
th:field="*{repeatForXWeeks}"
placeholder="Number of weeks to repeat, e.g. 5" />
placeholder="Number of Recurrences" />
<div class="tooltip">
<button class="tooltip__control fa-solid fa-question"></button>
<p role="tooltip" style="width: 30rem; white-space: initial">
The number you provide dictates how many sessions will be created in the following weeks, beyond the session
you're planning for the specified date.
</p>
</div>
</div>
</div>
</div>
......
......@@ -42,7 +42,53 @@
</th:block>
</div>
<div class="flex vertical">
<div class="flex align-center">
<section id="general-information" class="flex wrap align-center align-stretch content-wrapper mb-3 gap-2">
<div class="surface content-wrapper">
<div class="surface__content flex vertical align-center gap-0">
<h3 class="font-300">
<span id="card-request-count">-1</span>
Requests
</h3>
<h4 class="font-100">
<span id="card-submission-count">-1</span>
Submissions |
<span id="card-question-count">-1</span>
Questions
</h4>
</div>
</div>
<div class="surface content-wrapper">
<div class="surface__content flex justify-center">
<h3 class="font-400">
<span id="card-enqueued-count">-1</span>
People Enqueued
</h3>
</div>
</div>
<div class="surface content-wrapper">
<div class="surface__content flex justify-center">
<h3 class="font-400">
Average Waiting Time:
<span id="card-waiting-time">-1</span>
</h3>
</div>
</div>
<div class="surface content-wrapper">
<div class="surface__content flex justify-center">
<h3 class="font-400">
Average Processing Time:
<span id="card-processing-time">-1</span>
</h3>
</div>
</div>
</section>
</div>
<div class="flex vertical mb-3">
<table class="table" id="assistant-frequency-table" data-style="surface">
<tr class="table__header">
<th>Student Name</th>
......@@ -52,6 +98,15 @@
</table>
</div>
<section id="chart-sec-1" class="flex wrap mb-3">
<div class="surface p-0">
<h5 class="surface__header">Assignment Breakdown</h5>
<div class="surface__content">
<canvas id="assignment-frequency-canvas" height="300px" width="400px"></canvas>
</div>
</div>
</section>
<script th:inline="javascript" type="text/javascript">
const TIMEOUT_INTERVAL_TIME = 10000;
let timeOutInterval;
......@@ -71,15 +126,38 @@
* Asynchronously fetches data and updates the statistics.
*/
function update() {
const qSessionId = /*[[@{qSession.id}]]*/ -1;
const qSessionId = [[${qSession.id}]];
const genericFilterForm = $("#generic-filter-form");
$.ajax({
type: "GET",
data: genericFilterForm ? genericFilterForm.serialize() : "",
url: /*[[@{/lab/{id}/status/assistant/freq(id = ${qSession.id})}]]*/ `/lab/${qSessionId}`,
success: updateAssistantFrequencyTable($("#assistant-frequency-table")),
});
const generalUrl = `/lab/${qSessionId}/status/general`;
fetch(withParameters(generalUrl, genericFilterForm))
.then(response => response.json())
.then(updateGeneralInformation($("#general-information")))
const assistantFreqUrl = `/lab/${qSessionId}/status/assistant/freq/`;
fetch(withParameters(assistantFreqUrl, genericFilterForm))
.then(response => response.json())
.then(updateAssistantFrequencyTable($("#assistant-frequency-table")));
const assignmentFreqUrl = `/lab/${qSessionId}/status/assignment/freq`
fetch(withParameters(assignmentFreqUrl, genericFilterForm))
.then(response => response.json())
.then(updateAssignmentFrequencyChart($("#assignment-frequency-canvas")));
}
/**
* Appends parameters to a URL specified by the user using the form.
*
* @param url The url to append to
* @param paramForm The form to consider.
*/
function withParameters(url, paramForm) {
if (paramForm) {
url += "?";
url += new URLSearchParams(paramForm.serialize());
}
return url;
}
</script>
</section>
......
......@@ -18,32 +18,39 @@
package nl.tudelft.queue.controller;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static test.JsonContentMatcher.jsonContent;
import java.util.List;
import javax.transaction.Transactional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import nl.tudelft.queue.dto.view.statistics.AssistantSessionStatisticsViewDto;
import nl.tudelft.queue.dto.view.statistics.session.GeneralSessionStatisticsViewDto;
import nl.tudelft.queue.model.labs.RegularLab;
import test.BaseMockConfig;
import nl.tudelft.queue.repository.LabRepository;
import test.TestDatabaseLoader;
import test.test.TestQueueApplication;
@Transactional
@AutoConfigureMockMvc
@SpringBootTest(classes = TestQueueApplication.class)
@ContextConfiguration(classes = BaseMockConfig.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
class SessionStatusControllerTest {
@Autowired
......@@ -52,14 +59,19 @@ class SessionStatusControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private LabRepository labRepository;
RegularLab oopNowRegLab1;
RegularLab rlOopNowSharedLab;
@BeforeEach
void init() {
db.mockAll();
oopNowRegLab1 = db.getOopNowRegularLab1();
rlOopNowSharedLab = db.getRlOopNowSharedLab();
}
@Test
......@@ -84,10 +96,39 @@ class SessionStatusControllerTest {
}
@Test
@WithUserDetails("student55")
void studentsAreNotAllowedToViewStats() throws Exception {
mvc.perform(get("/lab/" + oopNowRegLab1.getId() + "/status/assistant/freq"))
.andExpect(status().isForbidden());
@WithUserDetails("admin")
void testGeneralStatisticsEndpoint() throws Exception {
mvc.perform(get("/lab/{labId}/status/general", oopNowRegLab1.getId()))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonContent(GeneralSessionStatisticsViewDto.class)
.test(dto -> assertThat(dto.getNumRequests())
.isEqualTo(oopNowRegLab1.getRequests().size())));
}
@Test
@WithUserDetails("admin")
void testAssignmentCountEndpoint() throws Exception {
mvc.perform(get("/lab/{labId}/status/assignment/freq", oopNowRegLab1.getId()))
.andExpect(status().is2xxSuccessful())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonContent(List.class)
.test(list -> assertThat(list).hasSize(1)));
}
@ParameterizedTest
@MethodSource(value = "protectedEndpoints")
void testWithoutUserDetailsIsForbidden(MockHttpServletRequestBuilder request) throws Exception {
mvc.perform(request.with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/login"));
}
private static List<MockHttpServletRequestBuilder> protectedEndpoints() {
return List.of(
get("/lab/{sessionId}/status/general", 2), get("/lab/{sessionId}/status/assistant/freq", 2),
get("/lab/{sessionId}/status/assignment/freq", 2));
}
}
......@@ -97,6 +97,10 @@ public class RequestServiceTest {
private CapacitySession session;
private EditionDetailsDTO oopNow;
private EditionDetailsDTO rlNow;
private RegularLab oopNowRegularLab1;
private SlottedLab oopNowSlottedLab1;
......@@ -172,6 +176,10 @@ public class RequestServiceTest {
rlOopNowSharedLabRequests = db.getRlOopNowSharedLabRequests();
oopNow = db.getOopNow();
rlNow = db.getRlNow();
sApiMocker.save(lcSessionNow);
sApiMocker.save(lcSessionOld1);
sApiMocker.save(lcSessionOld2);
......@@ -421,4 +429,23 @@ public class RequestServiceTest {
.containsExactly(rlOopNowSharedLabRequests);
}
@Test
void labRequestWithoutEditionFilterYieldsOriginalListThroughEditionFetch() {
assertThat(rs.getLabRequestsForEditions(Arrays.asList(rlOopNowSharedLabRequests), Set.of()))
.containsExactlyElementsOf(Arrays.asList(rlOopNowSharedLabRequests));
assertThat(rs.getLabRequestsForEditions(Arrays.asList(rlOopNowSharedLabRequests),
Set.of(oopNow.getId(), rlNow.getId())))
.containsExactlyElementsOf(Arrays.asList(rlOopNowSharedLabRequests));
}
@Test
void labRequestWithSingleEditionFilterOnlyYieldsRequestsForThatEdition() {
assertThat(rs.getLabRequestsForEditions(Arrays.asList(rlOopNowSharedLabRequests),
Set.of(oopNow.getId()))).containsExactlyElementsOf(Arrays.asList(rlOopNowSharedLabRequests));
assertThat(
rs.getLabRequestsForEditions(Arrays.asList(rlOopNowSharedLabRequests), Set.of(rlNow.getId())))
.isEmpty();
}
}
......@@ -21,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
......@@ -34,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import nl.tudelft.labracore.api.dto.AssignmentDetailsDTO;
import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.queue.model.LabRequest;
......@@ -62,6 +64,8 @@ class SessionStatusServiceTest {
LabRequest[] oopNowRegularLab1Requests;
AssignmentDetailsDTO[] oopNowAssignments;
PersonSummaryDTO oopTeacher1;
@BeforeEach
......@@ -70,15 +74,14 @@ class SessionStatusServiceTest {
oopNowRegularLab1 = db.getOopNowRegularLab1();
oopNowRegularLab1Requests = db.getOopNowRegularLab1Requests();
oopNowAssignments = db.getOopNowAssignments();
rlOopNowSharedLab = db.getRlOopNowSharedLab();
oopNow = db.getOopNow();
oopTeacher1 = db.getOopNowTeacher().getPerson();
}
@Test
void gettingTimeSinceLastInteractionWorksAsExpected() {
// this test pretty much replicates the src, but it tests boundaries so...
int countOfTaken = (int) oopNowRegularLab1.getRequests().stream()
.flatMap(req -> req.getEventInfo().getEvents().stream()
.filter(event -> event instanceof EventWithAssistant))
......@@ -102,4 +105,18 @@ class SessionStatusServiceTest {
}
@Test
void testAssignmentCountDoesNotFailForNoRequests() {
assertThat(sessionStatusService.countAssignmentFreqs(Collections.emptyList(),
Arrays.asList(oopNowAssignments))).hasSize(oopNowAssignments.length);
}
@Test
void testAssignmentCountIsGeneratedCorrectly() {
var sut = sessionStatusService.countAssignmentFreqs(oopNowRegularLab1.getRequests(),
Arrays.asList(oopNowAssignments));
assertThat(sut).hasSize(oopNowAssignments.length).isNotEmpty();
assertThat(sut.get(0).getAssignmentName()).isEqualTo("Assignment 0");
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment