Skip to content
Snippets Groups Projects
Commit 37eb30ae authored by Otto Visser's avatar Otto Visser
Browse files

Merge branch 'prevent-enqueue-without-group' into 'development'

Prevent enqueueing without a group if groups are required

See merge request !693
parents fce59ba5 36fe4b94
Branches
Tags
2 merge requests!694Development,!693Prevent enqueueing without a group if groups are required
......@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Course editions not created in Queue will now not be displayed until the teacher 'unhides' them. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx)
- Shared editions are now displayed properly as upcoming/finished/archived. [@mmadara](https://gitlab.ewi.tudelft.nl/mmadara)
- If a module already has groups, students now need to join a group before enqueueing. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx)
### Fixed
- Session names in the edition export did not correspond to the correct sessions. [@rwbackx](https://gitlab.ewi.tudelft.nl/rwbackx)
......
......@@ -18,10 +18,8 @@
package nl.tudelft.queue.controller;
import nl.tudelft.labracore.api.AssignmentControllerApi;
import nl.tudelft.labracore.api.dto.AssignmentCreateDTO;
import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
import nl.tudelft.labracore.api.dto.ModuleDetailsDTO;
import nl.tudelft.labracore.api.dto.ModuleSummaryDTO;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.queue.cache.AssignmentCacheManager;
import nl.tudelft.queue.cache.EditionCacheManager;
import nl.tudelft.queue.cache.ModuleCacheManager;
......@@ -50,6 +48,9 @@ public class AssignmentController {
@Autowired
private AssignmentControllerApi aApi;
@Autowired
private StudentGroupControllerApi sgApi;
/**
* Gets the page for creating an assignment. This is a basic page with only the most basic of properties
* for an assignment. Further adjustments or creation options should be made available in Portal or later
......@@ -141,6 +142,24 @@ public class AssignmentController {
return "redirect:/edition/" + module.getEdition().getId() + "/modules";
}
/**
* Get the page with a list of groups to join for an assignment.
*
* @param assignmentId The id of the assignment
* @return The group page
*/
@GetMapping("/assignment/{assignmentId}/groups")
public String getAssignmentGroups(@PathVariable Long assignmentId, Model model) {
AssignmentDetailsDTO assignment = aCache.getRequired(assignmentId);
ModuleDetailsDTO module = mCache.getRequired(assignment.getModule().getId());
model.addAttribute("module", module);
model.addAttribute("edition", eCache.getRequired(module.getEdition().getId()));
model.addAttribute("groups", sgApi.getAllGroupsInModule(module.getId()).collectList().block());
return "module/groups";
}
/**
* Adds attributes for the create assignment page to the given model.
*
......
......@@ -32,6 +32,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.transaction.Transactional;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
import nl.tudelft.labracore.lib.security.user.Person;
......@@ -134,12 +135,18 @@ public class LabController {
@Autowired
private SessionCacheManager sCache;
@Autowired
private StudentGroupCacheManager sgCache;
@Autowired
private RoomCacheManager rCache;
@Autowired
private RoleDTOService roleService;
@Autowired
private StudentGroupControllerApi sgApi;
@Autowired
private HttpSession session;
......@@ -774,6 +781,17 @@ public class LabController {
return edition.getId();
}));
Set<Long> alreadyInGroup = sgCache.getByPerson(person.getId()).stream()
.map(g -> g.getModule().getId()).collect(Collectors.toSet());
Set<Long> hasEmptyGroups = qSession.getModules().stream()
.filter(m -> !alreadyInGroup.contains(m))
.filter(m -> sgApi.getAllGroupsInModule(m).any(g -> g.getMemberUsernames().isEmpty())
.block())
.collect(Collectors.toSet());
model.addAttribute("needToJoinGroup",
allowedAssignments.stream().filter(a -> hasEmptyGroups.contains(a.getModule().getId()))
.map(AssignmentDetailsDTO::getId).collect(Collectors.toSet()));
model.addAttribute("assignments", assignments);
model.addAttribute("notEnqueueAble", notEnqueueAble);
model.addAttribute("types", lab.getAllowedRequests().stream()
......
/*
* Queue - A Queueing system that can be used to handle labs in higher education
* Copyright (C) 2016-2021 Delft University of Technology
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package nl.tudelft.queue.controller;
import lombok.AllArgsConstructor;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.queue.cache.ModuleCacheManager;
import nl.tudelft.queue.cache.StudentGroupCacheManager;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@AllArgsConstructor
@RequestMapping("group")
public class StudentGroupController {
private ModuleCacheManager mCache;
private StudentGroupCacheManager sgCache;
private StudentGroupControllerApi sgApi;
/**
* Joins the specified group.
*
* @param id The id of the group to join
* @return Redirects to edition page
*/
@PostMapping("{id}/join")
@PreAuthorize("@permissionService.canJoinGroup(#id)")
public String joinGroup(@AuthenticatedPerson Person person, @PathVariable Long id) {
sgApi.addMemberToGroup(id, person.getId()).block();
return "redirect:/edition/"
+ mCache.getRequired(sgCache.getRequired(id).getModule().getId()).getEdition().getId();
}
}
......@@ -784,4 +784,17 @@ public class PermissionService {
.anyMatch(m -> m.getId().equals(person.getId()))
|| isAdmin());
}
/**
* Checks if the authenticated user can join a given group.
*
* @param groupId The id of the group to join
* @return True iff the user can join the group
*/
public boolean canJoinGroup(long groupId) {
StudentGroupDetailsDTO group = sgCache.getRequired(groupId);
ModuleDetailsDTO module = mCache.getRequired(group.getModule().getId());
return withAuthenticatedUser(person -> withRole(module.getEdition().getId(), person.getId(),
role -> group.getMembers().size() < group.getCapacity()));
}
}
......@@ -88,6 +88,16 @@
<a class="link" th:href="@{/edition/{id}/enrol(id=${notEnqueueAble.get(entry.key)})}">here</a>
to enrol.
</div>
<div
th:each="entry : ${assignments}"
th:if="${needToJoinGroup.contains(entry.key)}"
class="colour-error"
style="display: none"
th:id="|need-group-${entry.key}|">
This assignment uses groups. Join a group
<a class="link" th:href="@{/assignment/{id}/groups(id=${entry.key})}">here</a>
first to enqueue.
</div>
<div class="colour-error" th:if="${#fields.hasErrors('assignment')}" th:errors="*{assignment}">Assignment error</div>
</div>
......@@ -206,6 +216,16 @@
}
}
function checkGroup(val) {
const enqueue = document.getElementById("enqueue");
const error = document.getElementById(`need-group-${val}`);
enqueue.removeAttribute("disabled");
if (error) {
error.style.setProperty("display", "block");
enqueue.setAttribute("disabled", "");
}
}
document.addEventListener("ComponentsLoaded", function () {
//<![CDATA[
const typesPerAssignment = /*[[${types}]]*/ { 1: ["bla"] };
......@@ -222,6 +242,7 @@
assignmentSelect.addEventListener("change", function () {
checkEnrolled(this.value);
checkGroup(this.value);
typeSelect.querySelectorAll("option").forEach(opt => opt.removeAttribute("selected"));
typeSelect.removeAttribute("disabled");
......
<!--
Queue - A Queueing system that can be used to handle labs in higher education
Copyright (C) 2016-2020 Delft University of Technology
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{edition/view}">
<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
<!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleDetailsDTO"-->
<body>
<section class="flex vertical gap-3" layout:fragment="subcontent">
<h3 class="font-500" th:text="|${module.name} - Join a group|"></h3>
<table class="table" data-style="surface">
<tr class="table__header">
<td>Group</td>
<td>Capacity</td>
<td></td>
</tr>
<tr th:each="group : ${groups}" th:if="${group.memberUsernames.size() < group.capacity}">
<td th:text="${group.name}"></td>
<td>
<span class="chip" th:text="|${group.memberUsernames.size()} / ${group.capacity}|"></span>
</td>
<td class="flex justify-end">
<form th:action="@{/group/{id}/join(id=${group.id})}" th:method="post">
<button type="submit" class="button p-min">Join</button>
</form>
</td>
</tr>
</table>
</section>
</body>
</html>
......@@ -31,6 +31,7 @@ import java.util.Set;
import javax.transaction.Transactional;
import nl.tudelft.labracore.api.SessionControllerApi;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.queue.dto.create.labs.CapacitySessionCreateDTO;
import nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO;
......@@ -72,6 +73,7 @@ import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import test.TestDatabaseLoader;
import test.test.TestQueueApplication;
......@@ -117,6 +119,9 @@ class LabControllerTest {
@Autowired
private LabRepository lr;
@Autowired
private StudentGroupControllerApi sgApi;
@Captor
private ArgumentCaptor<QueueSession<LabRequest>> queueSessionArgumentCaptor;
......@@ -151,6 +156,8 @@ class LabControllerTest {
when(sApi.addSharedSession(any())).thenReturn(Mono.just(667L));
when(sApi.patchSession(any(), any())).thenReturn(Mono.just(668L));
when(sgApi.getAllGroupsInModule(anyLong())).thenReturn(Flux.empty());
teacher1 = db.getTeachers()[1];
student5 = db.getStudents()[5];
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment