diff --git a/build.gradle.kts b/build.gradle.kts index 4c479e9ecad7ab912cb27f79bb2d47c60427ae4c..a9704e6d5cd1f904d565343cb110dd097c0e50e1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,8 +8,8 @@ version = "2.0.0" val javaVersion = JavaVersion.VERSION_17 -val libradorVersion = "1.0.3-SNAPSHOT6" -val labradoorVersion = "1.3.1-SNAPSHOT" +val libradorVersion = "1.1.0" +val labradoorVersion = "1.4.0-SNAPSHOT" val queryDslVersion = "4.4.0" // A definition of all dependencies and repositories where to find them that need to @@ -174,6 +174,15 @@ configure<SpotlessExtension> { trimTrailingWhitespace() endWithNewline() } + format("html") { + target("src/main/resources/templates/**/*.html", "src/main/resources/static/js/**/*.js", "src/main/resources/scss/**/*.scss") + prettier("2.6").config(mapOf( + "tabWidth" to 4, "semi" to true, + "printWidth" to 100, + "bracketSameLine" to true, + "arrowParens" to "avoid", + "htmlWhitespaceSensitivity" to "ignore")) + } } /////// TASKS /////// @@ -341,14 +350,11 @@ dependencies { implementation("org.webjars:webjars-locator-core") implementation("org.webjars:jquery:3.5.1") implementation("org.webjars:jquery-cookie:1.4.1-1") - implementation("org.webjars:bootstrap:4.3.1") - implementation("org.webjars:bootstrap-select:1.13.8") - implementation("org.webjars:font-awesome:5.10.1") + implementation("org.webjars:font-awesome:6.1.2") implementation("org.webjars:sockjs-client:1.5.1") implementation("org.webjars:stomp-websocket:2.3.4") implementation("org.webjars:handlebars:4.0.14") 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") diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000000000000000000000000000000000000..9e172fe6a9873bd89c213c6bc4ec1e9329b0ad2f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,9 @@ +#!/bin/sh +ssh-add ~/.ssh/rsa_personal +mv src/main/resources/application.yaml . +./gradlew clean +./gradlew assemble +mv application.yaml src/main/resources +scp build/libs/queue* rubenbackx.nl:/home/ruben/ +ssh rubenbackx.nl + diff --git a/src/main/java/nl/tudelft/queue/DevDatabaseLoader.java b/src/main/java/nl/tudelft/queue/DevDatabaseLoader.java index 1f8d2cae6f753571b51a6da45c6ebe3fcd080e1e..cf50fbdcc4f0e22deb2cf99d30742c984d5f2381 100644 --- a/src/main/java/nl/tudelft/queue/DevDatabaseLoader.java +++ b/src/main/java/nl/tudelft/queue/DevDatabaseLoader.java @@ -35,7 +35,7 @@ import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.ModuleControllerApi; import nl.tudelft.labracore.api.RoomControllerApi; import nl.tudelft.labracore.api.dto.*; -import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.queue.dto.create.embeddables.ExamLabConfigCreateDTO; import nl.tudelft.queue.dto.create.embeddables.SlottedLabConfigCreateDTO; import nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO; @@ -193,6 +193,7 @@ public class DevDatabaseLoader implements InitializingBean { private void initLabs() { oopOldL0 = ls.createSessions(RegularLabCreateDTO.builder() + .organisationalUnit(oopNow.getId()) .name("OOP Lab 0ld") .eolGracePeriod(15) .modules(Set.of(oopOldModuleA.getId())) @@ -202,9 +203,10 @@ public class DevDatabaseLoader implements InitializingBean { .map(RoomSummaryDTO::getId).collect(Collectors.toSet())) .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) .slot(new Slot(LocalDateTime.now(), LocalDateTime.now().plusMinutes(225).minusYears(1))) - .build(), oopNow.getId(), REGULAR).get(0); + .build(), REGULAR).get(0); oopL1 = ls.createSessions(RegularLabCreateDTO.builder() + .organisationalUnit(oopNow.getId()) .name("OOP Lab 1") .eolGracePeriod(15) .modules(Set.of(oopNowModuleA.getId())) @@ -214,9 +216,10 @@ public class DevDatabaseLoader implements InitializingBean { .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) .slot(new Slot(LocalDateTime.now().minusYears(1), LocalDateTime.now().plusMinutes(225))) - .build(), oopNow.getId(), REGULAR).get(0); + .build(), REGULAR).get(0); oopL2 = ls.createSessions(RegularLabCreateDTO.builder() + .organisationalUnit(oopNow.getId()) .name("OOP Lab 2") .eolGracePeriod(15) .modules(Set.of(oopNowModuleA.getId())) @@ -229,9 +232,10 @@ public class DevDatabaseLoader implements InitializingBean { .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) .slot(new Slot(LocalDateTime.now().plusWeeks(1), LocalDateTime.now().plusWeeks(1).plusMinutes(225))) - .build(), oopNow.getId(), REGULAR).get(0); + .build(), REGULAR).get(0); oopE = ls.createSessions(ExamLabCreateDTO.builder() + .organisationalUnit(oopNow.getId()) .name("Exam") .eolGracePeriod(15) .slottedLabConfig(SlottedLabConfigCreateDTO.builder() @@ -246,9 +250,10 @@ public class DevDatabaseLoader implements InitializingBean { .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) .slot(new Slot(LocalDateTime.now(), LocalDateTime.now().plusMinutes(225))) - .build(), oopNow.getId(), REGULAR).get(0); + .build(), REGULAR).get(0); adsL1I = ls.createSessions(RegularLabCreateDTO.builder() + .organisationalUnit(adsNow.getId()) .name("ADS Lab 1") .eolGracePeriod(15) .modules(Set.of(adsNowModuleI.getId())) @@ -259,9 +264,10 @@ public class DevDatabaseLoader implements InitializingBean { .map(RoomSummaryDTO::getId).collect(Collectors.toSet())) .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) .slot(new Slot(LocalDateTime.now(), LocalDateTime.now().plusMinutes(225))) - .build(), adsNow.getId(), REGULAR).get(0); + .build(), REGULAR).get(0); adsL1A = ls.createSessions(RegularLabCreateDTO.builder() + .organisationalUnit(adsNow.getId()) .name("Analysis Lab 1") .eolGracePeriod(15) .modules(Set.of(adsNowModuleA.getId())) @@ -271,9 +277,10 @@ public class DevDatabaseLoader implements InitializingBean { .map(RoomSummaryDTO::getId).collect(Collectors.toSet())) .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) .slot(new Slot(LocalDateTime.now(), LocalDateTime.now().plusMinutes(225))) - .build(), adsNow.getId(), REGULAR).get(0); + .build(), REGULAR).get(0); adsL2I = ls.createSessions(RegularLabCreateDTO.builder() + .organisationalUnit(adsNow.getId()) .name("ADS Lab 2") .eolGracePeriod(15) .modules(Set.of(adsNowModuleI.getId())) @@ -286,7 +293,7 @@ public class DevDatabaseLoader implements InitializingBean { .communicationMethod(CommunicationMethod.TA_VISIT_STUDENT) .slot(new Slot(LocalDateTime.now().plusWeeks(1), LocalDateTime.now().plusWeeks(1).plusMinutes(225))) - .build(), adsNow.getId(), REGULAR).get(0); + .build(), REGULAR).get(0); } private void initRequests() { diff --git a/src/main/java/nl/tudelft/queue/cache/ClusterCacheManager.java b/src/main/java/nl/tudelft/queue/cache/ClusterCacheManager.java deleted file mode 100644 index 84972774ba151f731a23fb103516782ebaef950c..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/ClusterCacheManager.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.cache; - -import java.util.List; -import java.util.stream.Collectors; - -import nl.tudelft.labracore.api.ClusterControllerApi; -import nl.tudelft.labracore.api.dto.ClusterDetailsDTO; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; - -@Component -@RequestScope -public class ClusterCacheManager extends CoreCacheManager<Long, ClusterDetailsDTO> { - @Autowired - private ClusterControllerApi cApi; - - @Override - protected List<ClusterDetailsDTO> fetch(List<Long> ids) { - return ids.stream() - .map(id -> cApi.getClusterById(id).block()) - .collect(Collectors.toList()); - } - - @Override - protected Long getId(ClusterDetailsDTO dto) { - return dto.getId(); - } -} diff --git a/src/main/java/nl/tudelft/queue/cache/CoreCacheManager.java b/src/main/java/nl/tudelft/queue/cache/CoreCacheManager.java deleted file mode 100644 index e1e309f37f2a340304aabf576948f99c1c606413..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/CoreCacheManager.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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.cache; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.springframework.http.HttpStatus; -import org.springframework.web.server.ResponseStatusException; - -import com.google.common.collect.Sets; - -public abstract class CoreCacheManager<ID, DTO> { - protected final Map<ID, DTO> cache = new ConcurrentHashMap<>(); - protected final Set<ID> empties = new HashSet<>(); - - /** - * Fetches the objects currently missing from the cache. These objects are fetched through a list of their - * IDs. - * - * @param ids The list of IDs of objects to fetch. - * @return The list of fetched objects. - */ - protected abstract List<DTO> fetch(List<ID> ids); - - /** - * Gets the ID of a particular instance of the data object. - * - * @param dto The DTO data object that will be cached. - * @return The ID of the DTO object. - */ - protected abstract ID getId(DTO dto); - - /** - * @return The maximum number of items to be fetched from Labracore in one go. - */ - protected int batchSize() { - return 128; - } - - /** - * Used to manually check the status of the cache and invalidate the cache if necessary. - */ - protected synchronized void validateCache() { - } - - /** - * Gets a list of DTOs from a list of ids. The list of ids gets broken up into already cached items and to - * be fetched items. The to be fetched are then fetched and the already cached are just fetched from - * cache. - * - * @param ids The ids to find matching DTOs for. - * @return The list of DTOs found to be matching. - */ - public List<DTO> get(List<ID> ids) { - validateCache(); - - List<ID> misses = ids.stream() - .filter(id -> !cache.containsKey(id) && !empties.contains(id)) - .collect(Collectors.toList()); - if (!misses.isEmpty()) { - forEachBatch(misses, batch -> registerImpl(new HashSet<>(batch), fetch(batch))); - } - - return ids.stream() - .map(cache::get) - .collect(Collectors.toList()); - } - - /** - * Gets a list of DTOs from a stream of IDs. This method is mostly used as a convenience method wrapping - * {@link #get(List)}. - * - * @param ids The stream of ids to be collected and requested from the cache. - * @return The list of DTOs with the given IDs. - */ - public List<DTO> get(Stream<ID> ids) { - return get(ids.collect(Collectors.toList())); - } - - /** - * Gets a single DTO from its id. If the DTO is already cached, no external lookup is done. - * - * @param id The id of the DTO to lookup. - * @return The requested DTO if it exists, otherwise nothing. - */ - public Optional<DTO> get(ID id) { - if (id == null) { - return Optional.empty(); - } - return Optional.of(get(List.of(id))) - .filter(l -> !l.isEmpty()) - .map(l -> l.get(0)); - } - - /** - * Gets the DTO with the given id or throws an exception with status code 404 if none such DTO could be - * found. - * - * @param id The id of the DTO to find. - * @return The found DTO with the given id. - */ - public DTO getOrThrow(ID id) { - var l = get(List.of(id)); - if (l == null || l.isEmpty()) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find DTO by id " + id); - } - return l.get(0); - } - - /** - * Registers an entry into the cache after validating the cache first. - * - * @param dto The DTO that is to be registered. - */ - public void register(DTO dto) { - validateCache(); - registerImpl(dto); - } - - /** - * Registers a list of entries into the cache after validating the cache first. - * - * @param dtos The DTOs that are to be registered. - */ - public void register(List<DTO> dtos) { - validateCache(); - registerImpl(dtos); - } - - /** - * Implementation of the register method. Registers an entry into the cache. - * - * @param dto The DTO that is to be registered. - */ - private void registerImpl(DTO dto) { - cache.put(getId(dto), dto); - registerAdditionally(dto); - } - - /** - * Implementation of the register method. Registers a list of entries into the cache. - * - * @param dtos The DTOs that are to be registered. - */ - private void registerImpl(List<DTO> dtos) { - dtos.forEach(this::registerImpl); - } - - /** - * Implementation of the register method. Registers a list of entries into the cache and also registers - * the IDs that were requested but not returned as 'empty' IDs. - * - * @param requests The set of requested IDs. - * @param dtos The list of DTOs returned from the request. - */ - private void registerImpl(Set<ID> requests, List<DTO> dtos) { - dtos.forEach(this::registerImpl); - empties.addAll(Sets.difference(requests, - dtos.stream().map(this::getId).collect(Collectors.toSet()))); - } - - /** - * Performs additional registers in other caches if necessary. - * - * @param dto The DTO that is being registered. - */ - protected void registerAdditionally(DTO dto) { - } - - /** - * Helper function for iterating over batches of items. The batch size is determined through the - * {@link #batchSize()} method in implementations of this class. Each created batch will be passed on to - * the consumer function f. - * - * @param ids The list of ids that are to be batched. - * @param f The consumer of each batch. - */ - private void forEachBatch(List<ID> ids, Consumer<List<ID>> f) { - List<ID> batch = new ArrayList<>(); - for (ID id : ids) { - batch.add(id); - - if (batch.size() >= batchSize()) { - f.accept(batch); - batch.clear(); - } - } - - if (!batch.isEmpty()) { - f.accept(batch); - } - } - -} diff --git a/src/main/java/nl/tudelft/queue/cache/CourseCacheManager.java b/src/main/java/nl/tudelft/queue/cache/CourseCacheManager.java deleted file mode 100644 index a841a89a44e23436f095b1e5b1fa29be257a2b5b..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/CourseCacheManager.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.cache; - -import java.util.List; -import java.util.stream.Collectors; - -import nl.tudelft.labracore.api.CourseControllerApi; -import nl.tudelft.labracore.api.dto.CourseDetailsDTO; -import nl.tudelft.labracore.api.dto.CourseSummaryDTO; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; - -@Component -@RequestScope -public class CourseCacheManager extends CoreCacheManager<Long, CourseDetailsDTO> { - @Autowired - private CourseControllerApi api; - - public List<CourseDetailsDTO> getAll() { - var courses = api.getAllCoursesById(api.getAllCourses().collectList().block().stream() - .map(CourseSummaryDTO::getId).collect(Collectors.toList())).collectList().block(); - register(courses); - - return courses; - } - - @Override - protected List<CourseDetailsDTO> fetch(List<Long> ids) { - return api.getAllCoursesById(ids).collectList().block(); - } - - @Override - protected Long getId(CourseDetailsDTO dto) { - return dto.getId(); - } -} diff --git a/src/main/java/nl/tudelft/queue/cache/EditionCollectionCacheManager.java b/src/main/java/nl/tudelft/queue/cache/EditionCollectionCacheManager.java deleted file mode 100644 index c1c37e0646697e80de01053b3ef7346c89b296a4..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/EditionCollectionCacheManager.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.cache; - -import java.util.List; - -import nl.tudelft.labracore.api.EditionCollectionControllerApi; -import nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; - -@Component -@RequestScope -public class EditionCollectionCacheManager extends CoreCacheManager<Long, EditionCollectionDetailsDTO> { - @Autowired - private EditionCollectionControllerApi api; - - @Override - protected List<EditionCollectionDetailsDTO> fetch(List<Long> ids) { - return api.getEditionCollectionsById(ids).collectList().block(); - } - - @Override - protected Long getId(EditionCollectionDetailsDTO dto) { - return dto.getId(); - } -} diff --git a/src/main/java/nl/tudelft/queue/cache/EditionRolesCacheManager.java b/src/main/java/nl/tudelft/queue/cache/EditionRolesCacheManager.java deleted file mode 100644 index ce9ebe998c178569cc96ed7e54e0bbeaf6b3062c..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/EditionRolesCacheManager.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.cache; - -import java.util.List; -import java.util.stream.Collectors; - -import lombok.AllArgsConstructor; -import lombok.Data; -import nl.tudelft.labracore.api.EditionControllerApi; -import nl.tudelft.labracore.api.dto.RolePersonDetailsDTO; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; - -@Component -@RequestScope -public class EditionRolesCacheManager extends CoreCacheManager<Long, EditionRolesCacheManager.RoleHolder> { - /** - * Holds a list of roles cached by the ID of the edition that was to be looked up. - */ - @Data - @AllArgsConstructor - public static class RoleHolder { - private Long id; - private List<RolePersonDetailsDTO> roles; - } - - private final EditionControllerApi eApi; - - public EditionRolesCacheManager(@Autowired EditionControllerApi eApi) { - this.eApi = eApi; - } - - @Override - protected List<RoleHolder> fetch(List<Long> ids) { - return ids.stream() - .map(id -> new RoleHolder(id, eApi.getEditionParticipants(id).collectList().block())) - .collect(Collectors.toList()); - } - - @Override - protected Long getId(RoleHolder roleHolder) { - return roleHolder.id; - } - - @Override - protected int batchSize() { - // Set batch size to MAX because we already do batches of n=1 in fetch. - return Integer.MAX_VALUE; - } -} diff --git a/src/main/java/nl/tudelft/queue/cache/ModuleCacheManager.java b/src/main/java/nl/tudelft/queue/cache/ModuleCacheManager.java deleted file mode 100644 index 3ab0cf2e4fd1ad0b0bd05a719a95396ff6bd7f81..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/ModuleCacheManager.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.cache; - -import java.util.List; - -import nl.tudelft.labracore.api.ModuleControllerApi; -import nl.tudelft.labracore.api.dto.ModuleDetailsDTO; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; - -@Component -@RequestScope -public class ModuleCacheManager extends CoreCacheManager<Long, ModuleDetailsDTO> { - @Autowired - private ModuleControllerApi api; - - @Override - protected List<ModuleDetailsDTO> fetch(List<Long> ids) { - return api.getModulesById(ids).collectList().block(); - } - - @Override - protected Long getId(ModuleDetailsDTO dto) { - return dto.getId(); - } - - @Override - protected int batchSize() { - return 1; - } -} diff --git a/src/main/java/nl/tudelft/queue/cache/PersonCacheManager.java b/src/main/java/nl/tudelft/queue/cache/PersonCacheManager.java deleted file mode 100644 index 476877f9846c3403a50f8da52fc2ee42719573a9..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/PersonCacheManager.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.cache; - -import java.util.List; - -import nl.tudelft.labracore.api.PersonControllerApi; -import nl.tudelft.labracore.api.dto.PersonSummaryDTO; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.ApplicationScope; - -@Component -@ApplicationScope -public class PersonCacheManager extends TimedCacheManager<Long, PersonSummaryDTO> { - @Autowired - private PersonControllerApi api; - - /** - * Creates a new person cache by setting the timeout on its timed cache implementation to 5 minutes. - */ - public PersonCacheManager() { - super(5 * 60 * 1000L); - } - - @Override - protected List<PersonSummaryDTO> fetch(List<Long> ids) { - return api.getPeopleById(ids).collectList().block(); - } - - @Override - protected Long getId(PersonSummaryDTO dto) { - return dto.getId(); - } -} diff --git a/src/main/java/nl/tudelft/queue/cache/RoleCacheManager.java b/src/main/java/nl/tudelft/queue/cache/RoleCacheManager.java deleted file mode 100644 index 277de71911c1c2bfc82d674407dd5461fc89d7b1..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/RoleCacheManager.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.cache; - -import java.util.List; -import java.util.stream.Collectors; - -import nl.tudelft.labracore.api.RoleControllerApi; -import nl.tudelft.labracore.api.dto.Id; -import nl.tudelft.labracore.api.dto.RoleDetailsDTO; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; - -@Component -@RequestScope -public class RoleCacheManager extends CoreCacheManager<Id, RoleDetailsDTO> { - @Autowired - private RoleControllerApi api; - - @Autowired - private PersonCacheManager pCache; - - @Override - protected List<RoleDetailsDTO> fetch(List<Id> ids) { - return api.getRolesById( - ids.stream().map(Id::getEditionId).collect(Collectors.toList()), - ids.stream().map(Id::getPersonId).collect(Collectors.toList())) - .collectList().block(); - } - - @Override - protected Id getId(RoleDetailsDTO dto) { - return dto.getId(); - } - - @Override - protected void registerAdditionally(RoleDetailsDTO dto) { - pCache.register(dto.getPerson()); - } -} diff --git a/src/main/java/nl/tudelft/queue/cache/RoomCacheManager.java b/src/main/java/nl/tudelft/queue/cache/RoomCacheManager.java deleted file mode 100644 index 6e4765c41a6808ae748bf910b3e1201225bbff2d..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/RoomCacheManager.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.cache; - -import java.util.List; -import java.util.stream.Collectors; - -import nl.tudelft.labracore.api.RoomControllerApi; -import nl.tudelft.labracore.api.dto.RoomDetailsDTO; -import nl.tudelft.labracore.api.dto.RoomSummaryDTO; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; - -@Component -@RequestScope -public class RoomCacheManager extends CoreCacheManager<Long, RoomDetailsDTO> { - @Autowired - private RoomControllerApi api; - - public List<RoomDetailsDTO> getAll() { - var rooms = api.getAllRoomsById(api.getAllRooms().collectList().block().stream() - .map(RoomSummaryDTO::getId).collect(Collectors.toList())).collectList().block(); - register(rooms); - - return rooms; - } - - @Override - protected List<RoomDetailsDTO> fetch(List<Long> ids) { - return api.getAllRoomsById(ids).collectList().block(); - } - - @Override - protected Long getId(RoomDetailsDTO dto) { - return dto.getId(); - } -} diff --git a/src/main/java/nl/tudelft/queue/cache/StudentGroupCacheManager.java b/src/main/java/nl/tudelft/queue/cache/StudentGroupCacheManager.java deleted file mode 100644 index 286f15067d80c7ca1a45d5f23d909926c33e5e3a..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/StudentGroupCacheManager.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.cache; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -import lombok.AllArgsConstructor; -import lombok.NoArgsConstructor; -import nl.tudelft.labracore.api.StudentGroupControllerApi; -import nl.tudelft.labracore.api.dto.RolePersonLayer1DTO; -import nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO; -import nl.tudelft.labracore.api.dto.StudentGroupSummaryDTO; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; - -@Component -@RequestScope -@NoArgsConstructor -@AllArgsConstructor -public class StudentGroupCacheManager extends CoreCacheManager<Long, StudentGroupDetailsDTO> { - private final Map<Long, List<StudentGroupDetailsDTO>> attemptedPersons = new HashMap<>(); - - @Autowired - private StudentGroupControllerApi api; - - @Autowired - private PersonCacheManager pCache; - - /** - * Gets all student groups of a single person by their id. - * - * @param personId The id of the person to lookup student groups for. - * @return The list of student group details fetched for this person. - */ - public List<StudentGroupDetailsDTO> getByPerson(Long personId) { - if (attemptedPersons.containsKey(personId)) { - return attemptedPersons.get(personId); - } - - var sgs = fetch(api.getGroupsForPerson(personId) - .map(StudentGroupSummaryDTO::getId) - .collectList().block()); - attemptedPersons.put(personId, sgs); - - return sgs; - } - - @Override - protected List<StudentGroupDetailsDTO> fetch(@Nullable List<Long> ids) { - if (ids == null || ids.isEmpty()) { - return List.of(); - } - return api.getStudentGroupsById(ids).collectList().block(); - } - - @Override - protected Long getId(StudentGroupDetailsDTO dto) { - return dto.getId(); - } - - @Override - protected void registerAdditionally(StudentGroupDetailsDTO dto) { - pCache.register(Objects.requireNonNull(dto.getMembers()).stream() - .map(RolePersonLayer1DTO::getPerson).collect(Collectors.toList())); - } -} diff --git a/src/main/java/nl/tudelft/queue/cache/TimedCacheManager.java b/src/main/java/nl/tudelft/queue/cache/TimedCacheManager.java deleted file mode 100644 index c4fd1645d4ebafa1878e150ea0422f10527c9801..0000000000000000000000000000000000000000 --- a/src/main/java/nl/tudelft/queue/cache/TimedCacheManager.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.cache; - -public abstract class TimedCacheManager<ID, DTO> extends CoreCacheManager<ID, DTO> { - /** - * The default timeout of the timed cache. - */ - public static final long DEFAULT_TIMEOUT = 60000L; - - /** - * The set timeout of the timed cache in milliseconds. This many milliseconds after the last cache - * invalidation, this cache is invalidated again. - */ - private final long timeout; - - /** - * The last time in milliseconds since epoch (system-time) that this cache was (in)validated. - * {@link #timeout} milliseconds after this timestamp, the cache is invalidated (again). - */ - private long lastValidation = System.currentTimeMillis(); - - /** - * Constructs a new timed cache with the timeout set to {@link #DEFAULT_TIMEOUT}. - */ - public TimedCacheManager() { - this.timeout = DEFAULT_TIMEOUT; - } - - /** - * Constructs a new timed cache with the timeout set to the given timeout. - * - * @param timeout The timeout to set internally. - */ - public TimedCacheManager(long timeout) { - this.timeout = timeout; - } - - @Override - protected synchronized void validateCache() { - long now = System.currentTimeMillis(); - if (lastValidation + timeout < now) { - cache.clear(); - empties.clear(); - lastValidation = now; - } - } -} diff --git a/src/main/java/nl/tudelft/queue/config/LibradorConfig.java b/src/main/java/nl/tudelft/queue/config/LibradorConfig.java index 9e3e93df89f34e855c10d44e78e9d589bacfd25e..156a104f29954334255e380cc98e2b794b5d1cd3 100644 --- a/src/main/java/nl/tudelft/queue/config/LibradorConfig.java +++ b/src/main/java/nl/tudelft/queue/config/LibradorConfig.java @@ -34,6 +34,7 @@ import org.springframework.context.annotation.Configuration; @EnableLibrador @Configuration public class LibradorConfig { + /** * Bean for the {@link IdMapperBuilder} that is used by Librador to create proper conversions within the * used {@link org.modelmapper.ModelMapper}. diff --git a/src/main/java/nl/tudelft/queue/controller/AdminController.java b/src/main/java/nl/tudelft/queue/controller/AdminController.java index d97482e0df5116397796e7ad4ad11c71afeadb61..f70b1eb401fc6ec1b3d7805a296fe8f265633f73 100644 --- a/src/main/java/nl/tudelft/queue/controller/AdminController.java +++ b/src/main/java/nl/tudelft/queue/controller/AdminController.java @@ -27,10 +27,10 @@ import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.ProgramControllerApi; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.api.dto.CourseCreateDTO; +import nl.tudelft.labracore.lib.cache.EditionCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; -import nl.tudelft.labracore.lib.security.user.Person; -import nl.tudelft.queue.cache.EditionCacheManager; -import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.dto.create.QueueCourseCreateDTO; import nl.tudelft.queue.model.labs.Lab; import nl.tudelft.queue.repository.LabRepository; diff --git a/src/main/java/nl/tudelft/queue/controller/AssignmentController.java b/src/main/java/nl/tudelft/queue/controller/AssignmentController.java index ca8421552088e8a66be989a7231fa5083bb82434..097f0c89fd81bb02711def088a2009a1fb1e4b9b 100644 --- a/src/main/java/nl/tudelft/queue/controller/AssignmentController.java +++ b/src/main/java/nl/tudelft/queue/controller/AssignmentController.java @@ -19,20 +19,22 @@ package nl.tudelft.queue.controller; import nl.tudelft.labracore.api.AssignmentControllerApi; import nl.tudelft.labracore.api.dto.AssignmentCreateDTO; +import nl.tudelft.labracore.api.dto.AssignmentPatchDTO; import nl.tudelft.labracore.api.dto.EditionDetailsDTO; import nl.tudelft.labracore.api.dto.ModuleDetailsDTO; -import nl.tudelft.queue.cache.AssignmentCacheManager; -import nl.tudelft.queue.cache.EditionCacheManager; -import nl.tudelft.queue.cache.ModuleCacheManager; +import nl.tudelft.labracore.lib.cache.AssignmentCacheManager; +import nl.tudelft.labracore.lib.cache.EditionCacheManager; +import nl.tudelft.labracore.lib.cache.ModuleCacheManager; +import nl.tudelft.labracore.lib.data.Assignment; +import nl.tudelft.labracore.lib.data.Module; import nl.tudelft.queue.dto.create.QueueAssignmentCreateDTO; +import nl.tudelft.queue.dto.patch.QueueAssignmentPatchDTO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.*; @Controller public class AssignmentController { @@ -49,89 +51,53 @@ public class AssignmentController { @Autowired private AssignmentControllerApi aApi; - /** - * 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 - * in Queue. - * - * @param moduleId The id of the module to add the assignment to. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/module/{moduleId}/assignment/create") - @PreAuthorize("@permissionService.canManageModule(#moduleId)") - public String getAssignmentCreatePage(@PathVariable Long moduleId, - Model model) { - return addCreateAssignmentAttributes(moduleId, new QueueAssignmentCreateDTO(moduleId), model); - } - /** * Handles a Post request to add a new assignment. The assignment is passed through a * {@link QueueAssignmentCreateDTO}. After adding the assignment successfully, the user is redirected back - * to the edition/modules page. If something goes wrong during conversion, the user is instead directed - * back to the assignment create page. + * to the edition/modules page. * - * @param mId The id of the module to add the assignment to. - * @param dto The dto containing information for creating the assignment. - * @param model The model to fill out for Thymeleaf template resolution. - * @return A redirect back to the edition/modules page. + * @param dto The dto containing information for creating the assignment. + * @return A redirect back to the edition/modules page. */ - @PostMapping("/module/{mId}/assignment/create") - @PreAuthorize("@permissionService.canManageModule(#mId)") - public String createAssignment(@PathVariable Long mId, - QueueAssignmentCreateDTO dto, - Model model) { - dto.setModuleId(mId); - + @PostMapping("/assignment") + @PreAuthorize("@permissionService.canManageModule(#dto.module)") + public String createAssignment(QueueAssignmentCreateDTO dto) { AssignmentCreateDTO create = dto.apply(); - if (dto.hasErrors()) { - return addCreateAssignmentAttributes(mId, dto, model); - } - aApi.addAssignment(create).block(); - - ModuleDetailsDTO module = mCache.getOrThrow(mId); - return "redirect:/edition/" + module.getEdition().getId() + "/modules"; + return "redirect:/edition/" + Module.byId(dto.getModule()).getEdition().value().getId() + "/modules"; } /** - * Gets the assignment removal page. This page is simply to confirm whether the user really wants to - * delete the assignment with the given id. + * Handles a Patch request to add a new assignment. The assignment is passed through a + * {@link QueueAssignmentCreateDTO}. After patching the assignment successfully, the user is redirected + * back to the edition/modules page. * - * @param assignmentId The id of the assignment that the user might want to delete. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. + * @param dto The dto containing information for creating the assignment. + * @return A redirect back to the edition/modules page. */ - @GetMapping("/assignment/{assignmentId}/remove") - @PreAuthorize("@permissionService.canManageAssignment(#assignmentId)") - public String getAssignmentRemovePage(@PathVariable Long assignmentId, - Model model) { - var assignment = aCache.getOrThrow(assignmentId); - var module = mCache.getOrThrow(assignment.getModule().getId()); - var edition = eCache.getOrThrow(module.getEdition().getId()); - - model.addAttribute("edition", edition); - model.addAttribute("assignment", assignment); - - return "assignment/remove"; + @PatchMapping("/assignment") + @PreAuthorize("@permissionService.canManageAssignment(#dto.assignment)") + public String editAssignment(QueueAssignmentPatchDTO dto) { + AssignmentPatchDTO patch = dto.apply(); + aApi.patchAssignment(dto.getAssignment(), patch).block(); + return "redirect:/edition/" + + Assignment.byId(dto.getAssignment()).getModule().flatMap(Module::getEdition).get().getId() + + "/modules"; } /** * Deletes the assignment with the given id and redirects back to the modules page for the relevant * edition. * - * @param assignmentId The id of the assignment to delete. - * @return A redirect back to the edition/modules page. + * @param id The id of the assignment to delete. + * @return A redirect back to the edition/modules page. */ - @PostMapping("/assignment/{assignmentId}/remove") - @PreAuthorize("@permissionService.canManageAssignment(#assignmentId)") - public String removeAssignment(@PathVariable Long assignmentId) { - var assignment = aCache.getOrThrow(assignmentId); - var module = mCache.getOrThrow(assignment.getModule().getId()); - - aApi.deleteAssignment(assignmentId).block(); - - return "redirect:/edition/" + module.getEdition().getId() + "/modules"; + @DeleteMapping("/assignment") + @PreAuthorize("@permissionService.canManageAssignment(#id)") + public String removeAssignment(@RequestParam Long id) { + aApi.deleteAssignment(id).block(); + return "redirect:/edition/" + Assignment.byId(id).getModule().get().getEdition().get().getId() + + "/modules"; } /** diff --git a/src/main/java/nl/tudelft/queue/cache/AssignmentCacheManager.java b/src/main/java/nl/tudelft/queue/controller/BuildingController.java similarity index 51% rename from src/main/java/nl/tudelft/queue/cache/AssignmentCacheManager.java rename to src/main/java/nl/tudelft/queue/controller/BuildingController.java index 5dbe488d4320dbdce903517d2acd743b63183c6b..609b75f01f3625645c23962d289f86388a9b4d74 100644 --- a/src/main/java/nl/tudelft/queue/cache/AssignmentCacheManager.java +++ b/src/main/java/nl/tudelft/queue/controller/BuildingController.java @@ -15,30 +15,30 @@ * 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.cache; +package nl.tudelft.queue.controller; -import java.util.List; +import nl.tudelft.labracore.api.dto.BuildingDetailsDTO; +import nl.tudelft.labracore.lib.data.Building; -import nl.tudelft.labracore.api.AssignmentControllerApi; -import nl.tudelft.labracore.api.dto.AssignmentDetailsDTO; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; +@Controller +@RequestMapping("/building") +public class BuildingController { -@Component -@RequestScope -public class AssignmentCacheManager extends CoreCacheManager<Long, AssignmentDetailsDTO> { - @Autowired - private AssignmentControllerApi api; - - @Override - protected List<AssignmentDetailsDTO> fetch(List<Long> ids) { - return api.getAllAssignmentsById(ids).collectList().block(); + /** + * Gets the details of a building. + * + * @param id The id of the building + * @return The details of the building + */ + @GetMapping("/{id}") + public @ResponseBody BuildingDetailsDTO getBuilding(@PathVariable Long id) { + return Building.byId(id).getDto(); } - @Override - protected Long getId(AssignmentDetailsDTO dto) { - return dto.getId(); - } } diff --git a/src/main/java/nl/tudelft/queue/controller/EditionController.java b/src/main/java/nl/tudelft/queue/controller/EditionController.java index 27e431c762304cc02c38c0867fb4707c6accdb63..3d4060f2ba54145c326ac191674eceba75564057 100644 --- a/src/main/java/nl/tudelft/queue/controller/EditionController.java +++ b/src/main/java/nl/tudelft/queue/controller/EditionController.java @@ -22,8 +22,8 @@ import static nl.tudelft.labracore.lib.LabracoreApiUtil.fromPageable; import static nl.tudelft.queue.PageUtil.toPage; import java.io.IOException; -import java.util.Comparator; -import java.util.List; +import java.time.LocalDateTime; +import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -32,15 +32,14 @@ import javax.validation.Valid; import nl.tudelft.labracore.api.*; import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.labracore.lib.cache.*; +import nl.tudelft.labracore.lib.data.Edition; +import nl.tudelft.labracore.lib.data.Module; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; -import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.*; -import nl.tudelft.queue.csv.EmptyCsvException; -import nl.tudelft.queue.csv.InvalidCsvException; -import nl.tudelft.queue.dto.create.CourseRequestCreateDTO; import nl.tudelft.queue.dto.create.QueueEditionCreateDTO; -import nl.tudelft.queue.dto.create.QueueRoleCreateDTO; +import nl.tudelft.queue.dto.patch.QueueEditionPatchDTO; import nl.tudelft.queue.dto.util.EditionFilterDTO; import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO; import nl.tudelft.queue.model.LabRequest; @@ -67,9 +66,6 @@ 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 org.springframework.web.reactive.function.client.WebClientResponseException; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller public class EditionController { @@ -101,6 +97,9 @@ public class EditionController { @Autowired private ModuleControllerApi mApi; + @Autowired + private BuildingControllerApi bApi; + @Autowired private CourseCacheManager cCache; @@ -154,19 +153,12 @@ public class EditionController { @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable, Model model) { var filter = es.getFilter("/editions"); - var editions = eApi - .getEditionsPageActiveOrTaughtBy(person.getId(), fromPageable(pageable), filter.getPrograms(), - filter.getNameSearch()) - .block(); + fillEditionListModel(person, pageable, filter, model); - eCache.register(editions.getContent()); - erCache.get(editions.getContent().stream().map(EditionDetailsDTO::getId)); - - model.addAttribute("editions", - new PageImpl<>(editions.getContent(), pageable, editions.getTotalElements())); - model.addAttribute("programs", cCache.getAll() - .stream().map(CourseDetailsDTO::getProgram).distinct() - .collect(Collectors.toList())); + model.addAttribute("programs", + cCache.get(cApi.getAllCourses().map(CourseSummaryDTO::getId).collectList().block()) + .stream().map(CourseDetailsDTO::getProgram).distinct() + .collect(Collectors.toList())); // TODO model.addAttribute("filter", filter); model.addAttribute("page", "catalog"); @@ -175,53 +167,70 @@ public class EditionController { } /** - * Submits the programme filters for the editions page. + * Gets the correct edition view, i.e. modules if an edition has no modules or no assignments, labs + * otherwise or if the user is a student. * - * @param programs The programmes to filter on - * @return The page to load + * @return A redirect to the correct view. */ - @PostMapping("/editions/filter") - public String submitFilters(@RequestParam(required = false) List<Long> programs, - @RequestParam(required = false) String nameSearch, - RedirectAttributes redirectAttributes) { - es.storeFilter(new EditionFilterDTO(programs, nameSearch), "/editions"); - redirectAttributes.addAttribute("page", 0); - return "redirect:/editions"; + @GetMapping("/edition/{id}") + public String getEditionView(@AuthenticatedPerson Person person, @PathVariable Long id) { + if (!ps.canViewEdition(id)) { + return "redirect:/edition/{id}/staff"; + } + Edition edition = Edition.byId(id); + List<Module> modules = Module + .byIds(edition.getModules().value().stream().map(Module::getId).toList()); + if (edition.getSessions().value().isEmpty() || + ((edition.getModules().value().isEmpty() + || modules.stream().allMatch(m -> m.getAssignments().value().isEmpty())) + && ps.canManageModules(id))) { + return "redirect:/edition/{id}/modules"; + } + return "redirect:/edition/{id}/labs"; } /** - * Gets the page for teachers to fill out when requesting a new course to be made. + * Gets the list of editions, filtered, and with a pageable. * - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. + * @param person The currently authenticated person asking for the editions page. + * @param pageable The pageable object representing the current page and size to display. + * @param model The model to fill out for Thymeleaf template resolution. + * @return The Thymeleaf template to resolve. */ - @GetMapping("/editions/request-course") - @PreAuthorize("@permissionService.isAdminOrTeacher()") - public String getRequestCoursePage(Model model) { - model.addAttribute("dto", new CourseRequestCreateDTO()); + @GetMapping("/editions/list") + public String getEditionListOnly(@AuthenticatedPerson Person person, + @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable, + Model model) { + var filter = es.getFilter("/editions"); + fillEditionListModel(person, pageable, filter, model); + return "edition/table"; + } - return "course/request"; + private void fillEditionListModel(Person person, Pageable pageable, EditionFilterDTO filter, + Model model) { + var editions = eApi + .getEditionsPageActiveOrTaughtBy(person.getId(), fromPageable(pageable), filter.getPrograms(), + filter.getNameSearch()) + .block(); + + eCache.register(editions.getContent()); + erCache.get(editions.getContent().stream().map(EditionDetailsDTO::getId)); + + model.addAttribute("editions", + new PageImpl<>(editions.getContent(), pageable, editions.getTotalElements())); } /** - * Gets the confirmation page for users that have the intention of enrolling in the given course edition. - * Entrance into this page is given whenever the authenticated person does not have a role in the given - * edition yet. If the person is already enrolled, they will be redirected to the catalog page. + * Submits the programme filters for the editions page. * - * @param editionId The id of the edition the user wants to enrol in. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. + * @param programs The programmes to filter on + * @return The page to load */ - @GetMapping("/edition/{editionId}/enrol") - @PreAuthorize("@permissionService.canEnrolForEdition(#editionId) " + - "|| @permissionService.canViewEdition(#editionId)") - public String getEditionEnrolView(@PathVariable Long editionId, Model model) { - if (!ps.canEnrolForEdition(editionId)) { - return "redirect:/editions"; - } - model.addAttribute("edition", eCache.getOrThrow(editionId)); - - return "edition/enrol"; + @PostMapping("/editions/filter") + public ResponseEntity<Void> submitFilters(@RequestParam(required = false) List<Long> programs, + @RequestParam(required = false) String nameSearch) { + es.storeFilter(new EditionFilterDTO(programs, nameSearch), "/editions"); + return ResponseEntity.ok().build(); } /** @@ -232,28 +241,35 @@ public class EditionController { * @param editionId The id of the edition the user wants to enrol in. * @return A redirect to the edition info page for the given edition edition. */ - @PostMapping("/edition/{editionId}/enrol") + @PostMapping("/edition/enrol") @PreAuthorize("@permissionService.canEnrolForEdition(#editionId)") - public String enrolForEdition(@AuthenticatedPerson Person person, - @PathVariable Long editionId) { + public String enrolForEdition(@AuthenticatedPerson Person person, @RequestParam Long editionId) { eApi.addStudentsToEdition(editionId, List.of(person.getUsername()), false).block(); return "redirect:/edition/" + editionId; } /** - * Gets the main course edition info panel page. This page shows the most generic information on a course - * edition taken from Labracore. + * Gets the course edition info panel on staff. This page should be shown to people that do not have + * permission to edit the participants list and cannot see all students. * - * @param editionId The id of the edition to display. + * @param editionId The id of the edition for which to display participants. * @param model The model to fill out for Thymeleaf template resolution. * @return The Thymeleaf template to resolve. */ - @GetMapping("/edition/{editionId}") - public String getEditionView(@PathVariable Long editionId, Model model) { + @GetMapping("/edition/{editionId}/staff") + public String getEditionStaffView(@PathVariable Long editionId, Model model) { + var roles = erCache.getOrThrow(editionId).getRoles().stream() + .sorted(Comparator.comparing(r -> r.getPerson().getDisplayName())) + .toList(); model.addAttribute("edition", eCache.getOrThrow(editionId)); - - return "edition/view/info"; + model.addAttribute("teachers", + roles.stream().filter(r -> r.getType() == RolePersonDetailsDTO.TypeEnum.TEACHER).toList()); + model.addAttribute("headTas", + roles.stream().filter(r -> r.getType() == RolePersonDetailsDTO.TypeEnum.HEAD_TA).toList()); + model.addAttribute("tas", + roles.stream().filter(r -> r.getType() == RolePersonDetailsDTO.TypeEnum.TA).toList()); + return "edition/view/staff"; } /** @@ -266,20 +282,54 @@ public class EditionController { */ @GetMapping("/edition/{editionId}/participants") @PreAuthorize("@permissionService.canManageParticipants(#editionId)") - public String getEditionParticipantsView(@PathVariable Long editionId, Model model, - @RequestParam(value = "student-search", required = false) String studentSearch, + public String getEditionParticipantsView(@AuthenticatedPerson Person person, @PathVariable Long editionId, + Model model, + @RequestParam(value = "search", required = false) String search, + @RequestParam(value = "roles", required = false) List<RolePersonDetailsDTO.TypeEnum> roleFilter, @PageableDefault(size = 25) Pageable pageable) { + fillParticipantsModel(editionId, model, search, roleFilter, pageable); + model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block()); + model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block()); + model.addAttribute("assignments", + mCache.get(eCache.getOrThrow(editionId).getModules().stream().map(ModuleSummaryDTO::getId)) + .stream().flatMap(m -> m.getAssignments().stream()).toList()); + return "edition/view/participants"; + } + + /** + * Gets the course edition participants table. + * + * @param editionId The id of the edition for which to display participants. + * @param model The model to fill out for Thymeleaf template resolution. + * @return The Thymeleaf template to resolve. + */ + @GetMapping("/edition/{editionId}/participants/list") + @PreAuthorize("@permissionService.canManageParticipants(#editionId)") + public String getEditionParticipantsListOnly(@PathVariable Long editionId, Model model, + @RequestParam(value = "search", required = false) String search, + @RequestParam(value = "roles", required = false) List<RolePersonDetailsDTO.TypeEnum> roleFilter, + @PageableDefault(size = 25) Pageable pageable) { + fillParticipantsModel(editionId, model, search, roleFilter, pageable); + return "edition/view/participants_table"; + } + + private void fillParticipantsModel(Long editionId, Model model, String search, + List<RolePersonDetailsDTO.TypeEnum> roleFilter, Pageable pageable) { var edition = eCache.getOrThrow(editionId); - var students = roleDTOService.students(edition); + var roles = erCache.getOrThrow(editionId).getRoles().stream() + .sorted(Comparator.comparing(r -> r.getPerson().getDisplayName())) + .sorted(Comparator.comparingInt((RolePersonDetailsDTO r) -> r.getType().ordinal()).reversed()) + .toList(); - if (studentSearch != null) { - students = es.studentsMatchingFilter(students, studentSearch); + if (roleFilter != null && !roleFilter.isEmpty()) { + roles = roles.stream().filter(r -> roleFilter.contains(r.getType())).toList(); + } + if (search != null) { + roles = es.rolesMatchingFilter(roles, search); } model.addAttribute("edition", edition); - model.addAttribute("students", toPage(pageable, students)); - - return "edition/view/participants"; + model.addAttribute("roles", toPage(pageable, roles)); } /** @@ -293,7 +343,8 @@ public class EditionController { */ @GetMapping("/edition/{editionId}/modules") @PreAuthorize("@permissionService.canViewEdition(#editionId)") - public String getEditionModulesView(@PathVariable Long editionId, Model model) { + public String getEditionModulesView(@AuthenticatedPerson Person person, @PathVariable Long editionId, + Model model) { var edition = eCache.getOrThrow(editionId); var modules = mCache.get(edition.getModules().stream().map(ModuleSummaryDTO::getId)); @@ -311,6 +362,11 @@ public class EditionController { model.addAttribute("groups", modules.stream() .collect(Collectors.toMap(ModuleDetailsDTO::getId, m -> sgCache.get(m.getGroups().stream().map(StudentGroupSmallSummaryDTO::getId))))); + model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block()); + model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block()); + model.addAttribute("assignments", + mCache.get(eCache.getOrThrow(editionId).getModules().stream().map(ModuleSummaryDTO::getId)) + .stream().flatMap(m -> m.getAssignments().stream()).toList()); return "edition/view/modules"; } @@ -326,10 +382,42 @@ public class EditionController { */ @GetMapping("/edition/{editionId}/labs") @PreAuthorize("@permissionService.canViewEdition(#editionId)") - public String getEditionSessionsView(@PathVariable Long editionId, + public String getEditionSessionsView(@AuthenticatedPerson Person person, @PathVariable Long editionId, + @RequestParam(required = false, defaultValue = "") List<QueueSessionType> queueSessionTypes, + @RequestParam(required = false, defaultValue = "") List<Long> modules, + Model model) { + fillSessionsModel(editionId, queueSessionTypes, modules, model); + model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block()); + model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block()); + model.addAttribute("assignments", + mCache.get(eCache.getOrThrow(editionId).getModules().stream().map(ModuleSummaryDTO::getId)) + .stream().flatMap(m -> m.getAssignments().stream()).toList()); + model.addAttribute("buildings", bApi.getAllBuildings().collectList().block()); + model.addAttribute("clusters", Edition.byId(editionId).getCohort().get().getClusters().value()); + + return "edition/view/labs"; + } + + /** + * Gets the course edition labs tables. + * + * @param editionId The id of the edition for which to display labs. + * @param model The model to fill out for Thymeleaf template resolution. + * @return The Thymeleaf template to resolve. + */ + @GetMapping("/edition/{editionId}/labs/list") + @PreAuthorize("@permissionService.canViewEdition(#editionId)") + public String getEditionSessionsViewListOnly(@PathVariable Long editionId, @RequestParam(required = false, defaultValue = "") List<QueueSessionType> queueSessionTypes, @RequestParam(required = false, defaultValue = "") List<Long> modules, Model model) { + fillSessionsModel(editionId, queueSessionTypes, modules, model); + + return "edition/view/labs_tables"; + } + + private void fillSessionsModel(Long editionId, List<QueueSessionType> queueSessionTypes, + List<Long> modules, Model model) { var edition = eCache.getOrThrow(editionId); // Make sure that session details are cached. @@ -337,21 +425,24 @@ public class EditionController { .map(SessionSummaryDTO::getId)); // Find all labs from the sessions, convert to summaries, and filter. - var labs = es.filterLabs( + List<QueueSessionSummaryDTO> labs = es.filterLabs( getLabSummariesFromSessions(sessions, s -> true), - queueSessionTypes, modules); + queueSessionTypes, modules) + .stream().sorted(Comparator.comparing(l -> l.getSlot().getOpensAt())) + .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); - // Sort all labs - labs = es.sortLabs(labs); + var now = LocalDateTime.now(); model.addAttribute("edition", edition); - model.addAttribute("labs", labs); - model.addAttribute("allLabTypes", QueueSessionType.values()); + model.addAttribute("currentSessions", labs.stream().filter(l -> l.getSlot().open()).toList()); + model.addAttribute("upcomingSessions", + labs.stream().filter(l -> l.getSlot().getOpensAt().isAfter(now)).toList()); + Collections.reverse(labs); + model.addAttribute("pastSessions", + labs.stream().filter(l -> l.getSlot().getClosesAt().isBefore(now)).toList()); model.addAttribute("queueSessionTypes", queueSessionTypes); model.addAttribute("allModules", edition.getModules()); model.addAttribute("modules", modules); - - return "edition/view/labs"; } /** @@ -363,129 +454,19 @@ public class EditionController { */ @GetMapping("/edition/{editionId}/questions") @PreAuthorize("@permissionService.canManageQuestions(#editionId)") - public String getEditionQuestions(@PathVariable Long editionId, Model model) { + public String getEditionQuestions(@AuthenticatedPerson Person person, @PathVariable Long editionId, + Model model) { model.addAttribute("edition", eCache.getOrThrow(editionId)); model.addAttribute("questions", qApi.getQuestionsByEdition(editionId).collectList().block()); + model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block()); + model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block()); + model.addAttribute("assignments", + mCache.get(eCache.getOrThrow(editionId).getModules().stream().map(ModuleSummaryDTO::getId)) + .stream().flatMap(m -> m.getAssignments().stream()).toList()); return "edition/view/questions"; } - /** - * Gets the participant add page. This page is used to add new participants to an edition. - * - * @param editionId The id of the edition to which a participant should be added. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/edition/{editionId}/participants/create") - @PreAuthorize("@permissionService.canManageParticipants(#editionId)") - public String getAddParticipantPage(@PathVariable Long editionId, Model model) { - model.addAttribute("edition", eCache.getOrThrow(editionId)); - model.addAttribute("role", new QueueRoleCreateDTO(editionId)); - - return "edition/create/participant"; - } - - /** - * Posts a single participant addition to the server. This action comes from the participant add page. - * - * @param editionId The id of the edition to which a participant should be added. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @PostMapping("/edition/{editionId}/participants/create") - @PreAuthorize("@permissionService.canManageParticipants(#editionId)") - public String createParticipant(@PathVariable Long editionId, Model model, - QueueRoleCreateDTO dto, RedirectAttributes redirectAttributes) { - // TODO: Figure out a way to get rid of these additional sets - dto.setEditionId(editionId); - - RoleCreateDTO create = dto.apply(); - if (dto.hasErrors()) { - model.addAttribute("edition", eCache.getOrThrow(editionId)); - model.addAttribute("role", dto); - - return "edition/create/participant"; - } - - try { - pApi.getPersonByUsername(dto.getUsername()) - .flatMap(person -> rApi.addRole(create.person(new PersonIdDTO().id(person.getId())))) - .block(); - } catch (WebClientResponseException e) { - redirectAttributes.addFlashAttribute("error", - String.format("Person with username: %s cannot be found. " + - "Make sure this person has logged into our system before", - dto.getUsername())); - return "redirect:/edition/" + editionId + "/participants/create"; - } - - return "redirect:/edition/" + editionId + "/participants"; - } - - /** - * Adds new participants to the course based on a CSV file that was uploaded by the user. - * - * @param editionId The id of the edition to which the users must be added. - * @param csv A CSV file containing both the netId and their role in the course. - * @param attributes Used to pass information when redirecting to Thymeleaf. - * @return A redirect to either the participants page when successfull, otherwise redirect ot - * create participants page - */ - @PostMapping("/edition/{editionId}/participants/import") - @PreAuthorize("@permissionService.canManageTeachers(#editionId)") - public String importParticipants(@PathVariable Long editionId, - @RequestParam("file") MultipartFile csv, - RedirectAttributes attributes) { - EditionDetailsDTO edition = eCache.getOrThrow(editionId); - - try { - es.addCourseParticipants(csv, edition); - return "redirect:/edition/" + editionId + "/participants"; - } catch (EmptyCsvException | InvalidCsvException e) { - attributes.addFlashAttribute("error", "We were unable to add parse the CSV file. "); - } - - return "redirect:/edition/" + editionId + "/participants/create"; - } - - /** - * Gets the participant remove page. This page is used to remove a specific participant from the given - * edition. - * - * @param editionId The id of the edition from which the participant should be removed. - * @param personId The id of the person who should be removed. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/edition/{editionId}/participants/{personId}/remove") - @PreAuthorize("@permissionService.canManageParticipants(#editionId)") - public String getRemoveParticipantPage(@PathVariable Long editionId, @PathVariable Long personId, - Model model) { - EditionDetailsDTO edition = eCache.getOrThrow(editionId); - - model.addAttribute("edition", edition); - model.addAttribute("role", rCache.getOrThrow(new Id() - .personId(personId).editionId(editionId))); - - return "edition/remove/participant"; - } - - /** - * Removes a participant with the given personId from the given edition. - * - * @param editionId The id of the edition from which the participant should be removed. - * @param personId The id of the person who should be removed. - * @return A redirect to the edition participants overview. - */ - @PostMapping("/edition/{editionId}/participants/{personId}/remove") - @PreAuthorize("@permissionService.canManageParticipants(#editionId)") - public String removeParticipant(@PathVariable Long editionId, @PathVariable Long personId) { - eApi.removePersonFromEdition(editionId, personId).block(); - - return "redirect:/edition/" + editionId + "/participants"; - } - /** * Blocks a person with the given id from the given edition. * @@ -501,22 +482,6 @@ public class EditionController { return "redirect:/edition/" + editionId + "/participants"; } - /** - * Gets the page to confirm for a student that they want to unenrol from the given course edition. - * - * @param editionId The id of the edition from which a student wants to unenrol. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/edition/{editionId}/leave") - @PreAuthorize("@permissionService.canLeaveEdition(#editionId)") - public String getParticipantLeavePage(@PathVariable Long editionId, - Model model) { - model.addAttribute("edition", eCache.getOrThrow(editionId)); - - return "edition/view/leave"; - } - /** * Processes a POST request from the user wanting to leave the given edition. Instead of deleting the role * of the user from the database entirely, we opt to block their role and thus disallow them access to the @@ -526,10 +491,9 @@ public class EditionController { * @param editionId The id of the edition that the student wants to leave. * @return A redirect back to the edition view page. */ - @PostMapping("/edition/{editionId}/leave") + @PostMapping("/edition/leave") @PreAuthorize("@permissionService.canLeaveEdition(#editionId)") - public String participantLeave(@AuthenticatedPerson Person user, - @PathVariable Long editionId) { + public String participantLeave(@AuthenticatedPerson Person user, @RequestParam Long editionId) { eApi.removePersonFromEdition(editionId, user.getId()).block(); return "redirect:/edition/" + editionId; @@ -584,35 +548,15 @@ public class EditionController { return "edition/view/status :: #assistant-table"; } - /** - * - * @param model The model to fill out for Thymeleaf template resolution. - * @return The thymeleaf template to resolve. - */ - @GetMapping("/edition/add") - @PreAuthorize("@permissionService.canCreateEdition()") - public String viewAddEdition(@AuthenticatedPerson Person person, Model model) { - QueueEditionCreateDTO edition = QueueEditionCreateDTO.builder().build(); - model.addAttribute("edition", edition); - - List<CourseSummaryDTO> courses = cApi.getAllCoursesByManager(person.getId()).collectList().block(); - model.addAttribute("courses", courses); - - List<CohortSummaryDTO> cohorts = cohortApi.getAllCohorts().collectList().block(); - model.addAttribute("cohorts", cohorts); - - return "edition/create"; - } - /** * Creates a new edition for a specific course. * * @param create The DTO used to create the edition * @return Redirect to the course page. */ - @PostMapping("/edition/add") + @PostMapping("/edition") @PreAuthorize("@permissionService.canCreateEdition(#create.course)") - public String addEdition(@Valid @ModelAttribute("edition") QueueEditionCreateDTO create) { + public String addEdition(@Valid QueueEditionCreateDTO create) { create.setEnrollability(EditionCreateDTO.EnrollabilityEnum.OPEN); EditionCreateDTO edition = create.apply(); @@ -621,6 +565,22 @@ public class EditionController { return "redirect:/edition/" + id; } + /** + * Patches an edition. + * + * @param patch The DTO used to patch the edition + * @return Redirect to the course page. + */ + @PatchMapping("/edition") + @PreAuthorize("@permissionService.canManageEdition(#patch.edition)") + public String editEdition(@Valid QueueEditionPatchDTO patch) { + EditionPatchDTO edition = patch.apply(); + + Long id = eApi.patchEdition(patch.getEdition(), edition).block(); + + return "redirect:/edition/" + id; + } + /** * Searches for questions when creating a request. * @@ -649,31 +609,6 @@ public class EditionController { return "redirect:/edition/{editionId}"; } - /** - * Gets the page to edit a question. - * - * @param editionId The id of the edition the question is for - * @param questionId The id of the question - * @param model The model to add details to - * @return The page to load - */ - @GetMapping("/edition/{editionId}/questions/{questionId}/edit") - @PreAuthorize("@permissionService.canManageQuestions(#editionId)") - public String getEditQuestionPage(@PathVariable Long editionId, @PathVariable Long questionId, - Model model) { - EditionDetailsDTO edition = eCache.getOrThrow(editionId); - - model.addAttribute("edition", edition); - model.addAttribute("question", qApi.getQuestionById(questionId).block()); - model.addAttribute("assignments", - mApi.getModulesById(edition.getModules().stream().map(ModuleSummaryDTO::getId) - .collect(Collectors.toList())).collectList().block().stream() - .flatMap(m -> m.getAssignments().stream()).collect(Collectors.toList())); - model.addAttribute("dto", new QuestionPatchDTO().assignment(new AssignmentIdDTO())); - - return "edition/edit/question"; - } - /** * Edits a question. * @@ -691,26 +626,6 @@ public class EditionController { return "redirect:/edition/{editionId}/questions"; } - /** - * Gets the delete question page - * - * @param editionId The id of the edition the question is for - * @param questionId The id of the question - * @param model The model to add details to - * @return The page to load - */ - @GetMapping("/edition/{editionId}/questions/{questionId}/remove") - @PreAuthorize("@permissionService.canManageQuestions(#editionId)") - public String getDeleteQuestionPage(@PathVariable Long editionId, @PathVariable Long questionId, - Model model) { - EditionDetailsDTO edition = eCache.getOrThrow(editionId); - - model.addAttribute("edition", edition); - model.addAttribute("question", qApi.getQuestionById(questionId).block()); - - return "edition/remove/question"; - } - /** * Deletes a question. * diff --git a/src/main/java/nl/tudelft/queue/controller/HistoryController.java b/src/main/java/nl/tudelft/queue/controller/HistoryController.java index 2dbfaf9b23370602199206c63d3bfe9a8db09481..e0842dd523a07222d0f8e92e7e55abee9f6d7a5c 100644 --- a/src/main/java/nl/tudelft/queue/controller/HistoryController.java +++ b/src/main/java/nl/tudelft/queue/controller/HistoryController.java @@ -26,12 +26,12 @@ import nl.tudelft.labracore.api.dto.EditionDetailsDTO; import nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO; import nl.tudelft.labracore.api.dto.SessionSummaryDTO; import nl.tudelft.labracore.api.dto.StudentGroupSummaryDTO; +import nl.tudelft.labracore.lib.cache.EditionCacheManager; +import nl.tudelft.labracore.lib.cache.EditionRolesCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; -import nl.tudelft.labracore.lib.security.user.Person; -import nl.tudelft.queue.cache.EditionCacheManager; -import nl.tudelft.queue.cache.EditionRolesCacheManager; -import nl.tudelft.queue.cache.PersonCacheManager; -import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.repository.LabRequestRepository; import nl.tudelft.queue.repository.QueueSessionRepository; import nl.tudelft.queue.service.RequestTableService; diff --git a/src/main/java/nl/tudelft/queue/controller/HomeController.java b/src/main/java/nl/tudelft/queue/controller/HomeController.java index 207c71a074e565b4b9bf4715d175a7f6427057d3..0edb7885f1b78674baad380897c99d593f52a75f 100644 --- a/src/main/java/nl/tudelft/queue/controller/HomeController.java +++ b/src/main/java/nl/tudelft/queue/controller/HomeController.java @@ -26,17 +26,15 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -import nl.tudelft.labracore.api.EditionControllerApi; -import nl.tudelft.labracore.api.PersonControllerApi; -import nl.tudelft.labracore.api.SessionControllerApi; +import nl.tudelft.labracore.api.*; import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.labracore.lib.cache.EditionCacheManager; +import nl.tudelft.labracore.lib.cache.EditionCollectionCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; -import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.EditionCacheManager; -import nl.tudelft.queue.cache.EditionCollectionCacheManager; -import nl.tudelft.queue.cache.PersonCacheManager; -import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.dto.view.FeedbackViewDTO; import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO; import nl.tudelft.queue.model.Feedback; @@ -78,6 +76,12 @@ public class HomeController { @Autowired private PersonControllerApi pApi; + @Autowired + private CourseControllerApi cApi; + + @Autowired + private CohortControllerApi cohortApi; + @Autowired private EditionCacheManager eCache; @@ -238,6 +242,8 @@ public class HomeController { model.addAttribute("finishedRoles", finishedRoles); model.addAttribute("archivedRoles", archivedRoles); model.addAttribute("sessions", calendarEntries); + model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block()); + model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block()); model.addAttribute("page", "my-courses"); @@ -293,13 +299,13 @@ public class HomeController { private void fillInFeedbackModel(Long assistantId, Person person, Model model, Pageable pageable) { var assistant = pCache.getOrThrow(assistantId); - Page<Feedback> feedback = assistantId.equals(person.getId()) - ? fr.findByAssistantAnonymised(assistantId, pageable) - : fr.findByAssistant(assistantId, pageable); + Page<Feedback> allFeedback = fr.findByAssistant(assistantId, pageable); + Page<Feedback> anonymisedFeedback = fr.findByAssistantAnonymised(assistantId, pageable); + Page<Feedback> feedback = assistantId.equals(person.getId()) ? anonymisedFeedback : allFeedback; model.addAttribute("assistant", assistant); - model.addAttribute("feedback", - feedback.map(fb -> View.convert(fb, FeedbackViewDTO.class))); + model.addAttribute("feedbackHidden", feedback.getTotalElements() < allFeedback.getTotalElements()); + model.addAttribute("feedback", feedback.map(fb -> View.convert(fb, FeedbackViewDTO.class))); model.addAttribute("stars", fs.countRatings(assistantId)); } diff --git a/src/main/java/nl/tudelft/queue/controller/LabController.java b/src/main/java/nl/tudelft/queue/controller/LabController.java index 2810c8ae39424ad7a51c4754a4281c35edb268f2..e8e3d442039871146376cdbb76400dfcb0e4055a 100644 --- a/src/main/java/nl/tudelft/queue/controller/LabController.java +++ b/src/main/java/nl/tudelft/queue/controller/LabController.java @@ -17,14 +17,12 @@ */ package nl.tudelft.queue.controller; +import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*; import static nl.tudelft.queue.service.LabService.SessionType.REGULAR; import static nl.tudelft.queue.service.LabService.SessionType.SHARED; import java.io.IOException; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -33,11 +31,15 @@ import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.transaction.Transactional; +import nl.tudelft.labracore.api.BuildingControllerApi; +import nl.tudelft.labracore.api.CohortControllerApi; import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.labracore.lib.cache.*; +import nl.tudelft.labracore.lib.data.CoreEntity; +import nl.tudelft.labracore.lib.data.Person; +import nl.tudelft.labracore.lib.data.Session; 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.dto.create.labs.CapacitySessionCreateDTO; import nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO; import nl.tudelft.queue.dto.create.labs.RegularLabCreateDTO; @@ -50,11 +52,11 @@ import nl.tudelft.queue.dto.patch.labs.CapacitySessionPatchDTO; import nl.tudelft.queue.dto.patch.labs.ExamLabPatchDTO; import nl.tudelft.queue.dto.patch.labs.RegularLabPatchDTO; import nl.tudelft.queue.dto.patch.labs.SlottedLabPatchDTO; +import nl.tudelft.queue.dto.view.QueueSessionViewDTO; import nl.tudelft.queue.dto.view.RequestViewDTO; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.Request; import nl.tudelft.queue.model.embeddables.AllowedRequest; -import nl.tudelft.queue.model.enums.QueueSessionType; import nl.tudelft.queue.model.enums.RequestType; import nl.tudelft.queue.model.enums.SelectionProcedure; import nl.tudelft.queue.model.labs.*; @@ -101,6 +103,9 @@ public class LabController { @Autowired private EditionCacheManager eCache; + @Autowired + private EditionRolesCacheManager erCache; + @Autowired private EditionCollectionCacheManager ecCache; @@ -116,6 +121,23 @@ public class LabController { @Autowired private RoleDTOService roleService; + @Autowired + private BuildingControllerApi bApi; + + @Autowired + private CohortControllerApi cohortApi; + + /** + * Gets session data as a JSON object. + * + * @param qSession The session + * @return The JSON representation of the session + */ + @GetMapping("/lab/json/{qSession}") + public @ResponseBody QueueSessionViewDTO<?> getSession(@PathEntity QueueSession<?> qSession) { + return qSession.toViewDTO(); + } + /** * Gets the page with information on the lab with the given id. This page is different for TAs than for * students. For students, this page shows their history of requests and their current requests. For TAs, @@ -133,13 +155,15 @@ public class LabController { public String getSessionView(@PathEntity QueueSession<?> qSession, @AuthenticatedPerson Person person, Model model) { + var filter = rts.checkAndStoreFilterDTO(null, "lab/" + qSession.getId()); + setEnqueuePageAttributes(qSession, model, person); // Either get all requests for the lab if the person is an assistant or just those specific to the person. - //noinspection Convert2MethodRef + var requests = ps.canViewSessionRequests(qSession.getId()) ? qSession.getRequests() + : qSession.getAllRequestsForPerson(person.getId()); model.addAttribute("requests", - rts.convertRequestsToView(ps.canViewSessionRequests(qSession.getId()) ? qSession.getRequests() - : qSession.getAllRequestsForPerson(person.getId())) + rts.convertRequestsToView(requests.stream().filter(filter::apply).toList()) .stream() .sorted(Comparator.comparing((RequestViewDTO<?> r) -> r.getCreatedAt()).reversed()) .collect(Collectors.toList())); @@ -148,10 +172,6 @@ public class LabController { Optional<?> currOptRequest = qSession.getOpenRequestForPerson(person.getId()); if (currOptRequest.isPresent()) { Request<?> currRequest = (Request<?>) currOptRequest.get(); - // If the user is currently being processed it should be looking at the request page - if (currRequest.getEventInfo().getStatus().isProcessing()) { - return "redirect:/request/" + currRequest.getId(); - } model.addAttribute("current", currRequest.toViewDTO()); model.addAttribute("currentDto", new ModelMapper().map(currRequest, RequestPatchDTO.class)); } @@ -162,8 +182,28 @@ public class LabController { .ifPresent(r -> model.addAttribute("selectionResult", r.toViewDTO())); } + model.addAttribute("filter", filter); model.addAttribute("modules", mCache.get(qSession.getModules().stream())); + model.addAttribute("assistants", + erCache.get(sCache.getOrThrow(qSession.getSession()).getEditions().stream() + .map(EditionSummaryDTO::getId)) + .stream().flatMap(e -> e.getRoles().stream()) + .filter(r -> Set.of(TEACHER, HEAD_TA, TA).contains(r.getType())) + .map(RolePersonDetailsDTO::getPerson).collect(Collectors.toList())); + + var editions = Session.byId(qSession.getSession()).getEditions().get(); + model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block()); + model.addAttribute("allModules", mCache + .get(editions.stream().flatMap(e -> e.getModules().value().stream().map(CoreEntity::getId)))); + model.addAttribute("allAssignments", + mCache.get(editions.stream().flatMap(e -> e.getDto().getModules().stream()) + .map(ModuleSummaryDTO::getId)) + .stream().flatMap(m -> m.getAssignments().stream()).toList()); + model.addAttribute("buildings", bApi.getAllBuildings().collectList().block()); + model.addAttribute("clusters", + editions.stream().flatMap(e -> e.getCohort().get().getClusters().value().stream()).toList()); + return "lab/view/" + qSession.getType().name().toLowerCase(); } @@ -176,7 +216,7 @@ public class LabController { * @return A redirect to the lab overview page. */ @Transactional - @PostMapping("/lab/{lab}/capacities") + @PatchMapping("/lab/{lab}/capacities") @PreAuthorize("@permissionService.canManageSession(#lab)") public String editLabCapacities(@PathEntity Lab lab, SlottedLabTimeSlotCapacityPatchDTO dto) { @@ -341,190 +381,115 @@ public class LabController { return "redirect:/lab/" + qSession.getId(); } - /** - * Gets the lab creation page. This page should be viewable by all teachers and managers (those that are - * allowed to create labs). This page allows for editing a lab before final creation of that lab. - * - * @param editionId The id of the edition to create the lab in. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/edition/{editionId}/lab/create") - @PreAuthorize("@permissionService.canManageSessions(#editionId)") - public String getLabCreateView(@PathVariable Long editionId, - @RequestParam QueueSessionType type, Model model) { - model.addAttribute("dto", type.newCreateDto()); - model.addAttribute("edition", eCache.getOrThrow(editionId)); - model.addAttribute("lType", type); - - setSessionEditingPageAttributes(List.of(editionId), model); - - return "lab/create/" + type.name().toLowerCase(); - } - - /** - * Gets the lab creation page. This page should be viewable by all teachers and managers (those that are - * allowed to create labs). This page allows for editing a lab before final creation of that lab. - * - * @param editionCollectionId The id of the edition collection to create lab in. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/shared-edition/{editionCollectionId}/lab/create") - @PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)") - public String getSharedLabCreateView(@PathVariable Long editionCollectionId, - @RequestParam QueueSessionType type, Model model) { - var editionCollection = ecCache.getOrThrow(editionCollectionId); - - model.addAttribute("dto", type.newCreateDto()); - model.addAttribute("ec", editionCollection); - model.addAttribute("lType", type); - - setSessionEditingPageAttributes(editionCollection.getEditions().stream() - .map(EditionSummaryDTO::getId).collect(Collectors.toList()), model); - - return "lab/create/" + type.name().toLowerCase(); - } - /** * Creates a new capacity session using the session create DTO provided through the body of the POST * request. Creates a session of the capacity type. * - * @param editionId The id of the edition this session is part of. - * @param dto The dto to apply to create a new session. - * @return A redirect to the created session page. + * @param dto The dto to apply to create a new session. + * @return A redirect to the created session page. */ - @PostMapping("/edition/{editionId}/lab/create/capacity") - @PreAuthorize("@permissionService.canManageSessions(#editionId)") - public String createCapacityLab(@PathVariable Long editionId, - CapacitySessionCreateDTO dto) { - ls.createSessions(dto, editionId, REGULAR); - return "redirect:/edition/" + editionId + "/labs"; + @PostMapping("/lab/capacity") + @PreAuthorize("@permissionService.canManageSessions(#dto.organisationalUnit)") + public String createCapacityLab(CapacitySessionCreateDTO dto) { + ls.createSessions(dto, REGULAR); + return "redirect:/edition/" + dto.getOrganisationalUnit() + "/labs"; } /** * Creates a new lab using the lab create DTO provided through the body of the POST request. Creates a lab * of the regular type. * - * @param editionId The id of the edition this lab is part of. - * @param dto The dto to apply to create a new lab. - * @return A redirect to the created lab page. + * @param dto The dto to apply to create a new lab. + * @return A redirect to the created lab page. */ - @PostMapping("/edition/{editionId}/lab/create/regular") - @PreAuthorize("@permissionService.canManageSessions(#editionId)") - public String createRegularLab(@PathVariable Long editionId, - RegularLabCreateDTO dto) { - ls.createSessions(dto, editionId, REGULAR); - return "redirect:/edition/" + editionId + "/labs"; + @PostMapping("/lab/regular") + @PreAuthorize("@permissionService.canManageSessions(#dto.organisationalUnit)") + public String createRegularLab(RegularLabCreateDTO dto) { + ls.createSessions(dto, REGULAR); + return "redirect:/edition/" + dto.getOrganisationalUnit() + "/labs"; } /** * Creates a new lab using the lab create DTO provided through the body of the POST request. Creates a lab * of the slotted type. * - * @param editionId The id of the edition this lab is part of. - * @param dto The dto to apply to create a new lab. - * @return A redirect to the created lab page. + * @param dto The dto to apply to create a new lab. + * @return A redirect to the created lab page. */ - @PostMapping("/edition/{editionId}/lab/create/slotted") - @PreAuthorize("@permissionService.canManageSessions(#editionId)") - public String createSlottedLab(@PathVariable Long editionId, - SlottedLabCreateDTO dto) { - ls.createSessions(dto, editionId, REGULAR); - return "redirect:/edition/" + editionId + "/labs"; + @PostMapping("/lab/slotted") + @PreAuthorize("@permissionService.canManageSessions(#dto.organisationalUnit)") + public String createSlottedLab(SlottedLabCreateDTO dto) { + ls.createSessions(dto, REGULAR); + return "redirect:/edition/" + dto.getOrganisationalUnit() + "/labs"; } /** * Creates a new lab using the lab create DTO provided through the body of the POST request. Creates a lab * of the exam type. * - * @param editionId The id of the edition this lab is part of. - * @param dto The dto to apply to create a new lab. - * @return A redirect to the created lab page. + * @param dto The dto to apply to create a new lab. + * @return A redirect to the created lab page. */ - @PostMapping("/edition/{editionId}/lab/create/exam") - @PreAuthorize("@permissionService.canManageSessions(#editionId)") - public String createExamLab(@PathVariable Long editionId, - ExamLabCreateDTO dto) { - ls.createSessions(dto, editionId, REGULAR); - return "redirect:/edition/" + editionId + "/labs"; + @PostMapping("/lab/exam") + @PreAuthorize("@permissionService.canManageSessions(#dto.organisationalUnit)") + public String createExamLab(ExamLabCreateDTO dto) { + ls.createSessions(dto, REGULAR); + return "redirect:/edition/" + dto.getOrganisationalUnit() + "/labs"; } /** * Creates a new lab using the lab create DTO provided through the body of the POST request. This method * creates one specifically for a shared edition. Creates a lab of the regular type. * - * @param editionCollectionId The id of the edition collection the lab will be part of. - * @param dto The dto to apply to create a new lab. - * @return A redirect to the created lab page. + * @param dto The dto to apply to create a new lab. + * @return A redirect to the created lab page. */ - @PostMapping("/shared-edition/{editionCollectionId}/lab/create/regular") - @PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)") - public String createRegularSharedLab(@PathVariable Long editionCollectionId, - RegularLabCreateDTO dto) { - ls.createSessions(dto, editionCollectionId, SHARED); - return "redirect:/shared-edition/" + editionCollectionId; + @PostMapping("/shared-edition/lab/regular") + @PreAuthorize("@permissionService.canManageSharedSessions(#dto.organisationalUnit)") + public String createRegularSharedLab(RegularLabCreateDTO dto) { + ls.createSessions(dto, SHARED); + return "redirect:/shared-edition/" + dto.getOrganisationalUnit(); } /** * Creates a new lab using the lab create DTO provided through the body of the POST request. This method * creates one specifically for a shared edition. Creates a lab of the slotted type. * - * @param editionCollectionId The id of the edition collection the lab will be part of. - * @param dto The dto to apply to create a new lab. - * @return A redirect to the created lab page. + * @param dto The dto to apply to create a new lab. + * @return A redirect to the created lab page. */ - @PostMapping("/shared-edition/{editionCollectionId}/lab/create/slotted") - @PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)") - public String createSlottedSharedLab(@PathVariable Long editionCollectionId, - SlottedLabCreateDTO dto) { - ls.createSessions(dto, editionCollectionId, SHARED); - return "redirect:/shared-edition/" + editionCollectionId; + @PostMapping("/shared-edition/lab/slotted") + @PreAuthorize("@permissionService.canManageSharedSessions(#dto.organisationalUnit)") + public String createSlottedSharedLab(SlottedLabCreateDTO dto) { + ls.createSessions(dto, SHARED); + return "redirect:/shared-edition/" + dto.getOrganisationalUnit(); } /** * Creates a new lab using the lab create DTO provided through the body of the POST request. This method * creates one specifically for a shared edition. Creates a lab of the exam type. * - * @param editionCollectionId The id of the edition collection the lab will be part of. - * @param dto The dto to apply to create a new lab. - * @return A redirect to the created lab page. - */ - @PostMapping("/shared-edition/{editionCollectionId}/lab/create/exam") - @PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)") - public String createExamSharedLab(@PathVariable Long editionCollectionId, - ExamLabCreateDTO dto) { - ls.createSessions(dto, editionCollectionId, SHARED); - return "redirect:/shared-edition/" + editionCollectionId; - } - - /** - * Gets the confirmation view for deleting a single lab. - * - * @param qSession The lab to delete. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. + * @param dto The dto to apply to create a new lab. + * @return A redirect to the created lab page. */ - @GetMapping("/lab/{qSession}/remove") - @PreAuthorize("@permissionService.canManageSession(#qSession)") - public String getSessionDeleteView(@PathEntity QueueSession<?> qSession, - Model model) { - model.addAttribute("qSession", qSession); - ls.setOrganizationInModel(qSession, model); - - return "lab/remove"; + @PostMapping("/shared-edition/lab/exam") + @PreAuthorize("@permissionService.canManageSharedSessions(#dto.organisationalUnit)") + public String createExamSharedLab(ExamLabCreateDTO dto) { + ls.createSessions(dto, SHARED); + return "redirect:/shared-edition/" + dto.getOrganisationalUnit(); } /** * Handles a POST request to the session delete endpoint by setting the deleted at date for the given * session. * - * @param qSession The session to delete. - * @return A redirect back to the edition lab overview. + * @param id The id of the session to delete. + * @return A redirect back to the edition lab overview. */ - @PostMapping("/lab/{qSession}/remove") - @PreAuthorize("@permissionService.canManageSession(#qSession)") - public String deleteSession(@PathEntity QueueSession<?> qSession) { + @DeleteMapping("/lab") + @PreAuthorize("@permissionService.canManageSession(#id)") + public String deleteSession(@RequestParam Long id) { + QueueSession<?> qSession = qsRepository.findByIdOrThrow(id); ls.deleteSession(qSession); var session = sCache.getOrThrow(qSession.getSession()); @@ -532,108 +497,64 @@ public class LabController { return "redirect:/edition/" + session.getEditions().get(0).getId() + "/labs"; } - /** - * Gets the lab creation page copied from the lab targeted in the url of this request. - * - * @param qSession The lab that is targeted for copying. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - // TODO: Check whether this permission cannot be better: currently one can copy a lab if they can manage that lab, - // but these permissions should be the same as creating a new lab. - @GetMapping("/lab/{qSession}/copy") - @PreAuthorize("@permissionService.canManageSession(#qSession)") - public String getLabCopyView(@PathEntity QueueSession<?> qSession, - Model model) { - var session = sCache.getOrThrow(qSession.getSession()); - - model.addAttribute("dto", qSession.copyLabCreateDTO(session)); - model.addAttribute("lType", qSession.getType()); - - setSessionEditingPageAttributes(qSession, model); - - return "lab/create/" + qSession.getType().name().toLowerCase(); - } - - /** - * Gets the lab editing page. - * - * @param qSession The lab that is to be edited by this page. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/lab/{qSession}/edit") - @PreAuthorize("@permissionService.canManageSession(#qSession)") - public String getLabUpdateView(@PathEntity QueueSession<?> qSession, Model model) { - model.addAttribute("dto", qSession.newPatchDTO()); - model.addAttribute("lab", qSession); - - setSessionEditingPageAttributes(qSession, model); - - return "lab/edit/" + qSession.getType().name().toLowerCase(); - } - /** * Updates the given lab entity using the given lab patch DTO. The lab in question is of the regular type. * - * @param lab The lab to update. * @param dto The dto representing the changes to apply to the lab. * @return A redirect to the lab view page. */ @Transactional - @PostMapping("/lab/{lab}/edit/regular") - @PreAuthorize("@permissionService.canManageSession(#lab)") - public String updateSession(@PathEntity RegularLab lab, RegularLabPatchDTO dto) { - ls.updateSession(dto, lab); - return "redirect:/lab/" + lab.getId(); + @PatchMapping("/lab/regular") + @PreAuthorize("@permissionService.canManageSession(#dto.id)") + public String updateSession(RegularLabPatchDTO dto) { + ls.updateSession(dto); + return "redirect:/lab/" + dto.getId(); } /** * Updates the given lab entity using the given lab patch DTO. The lab in question is of the slotted type. * - * @param lab The lab to update. * @param dto The dto representing the changes to apply to the lab. * @return A redirect to the lab view page. */ @Transactional - @PostMapping("/lab/{lab}/edit/slotted") - @PreAuthorize("@permissionService.canManageSession(#lab)") - public String updateSession(@PathEntity SlottedLab lab, SlottedLabPatchDTO dto) { - ls.updateSession(dto, lab); - return "redirect:/lab/" + lab.getId(); + @PatchMapping("/lab/slotted") + @PreAuthorize("@permissionService.canManageSession(#dto.id)") + public String updateSession(SlottedLabPatchDTO dto) { + ls.updateSession(dto); + return "redirect:/lab/" + dto.getId(); } /** * Updates the given lab entity using the given lab patch DTO. The lab in question is of the exam type. * - * @param lab The lab to update. * @param dto The dto representing the changes to apply to the lab. * @return A redirect to the lab view page. */ @Transactional - @PostMapping("/lab/{lab}/edit/exam") - @PreAuthorize("@permissionService.canManageSession(#lab)") - public String updateSession(@PathEntity ExamLab lab, ExamLabPatchDTO dto) { - ls.updateSession(dto, lab); - return "redirect:/lab/" + lab.getId(); + @PatchMapping("/lab/exam") + @PreAuthorize("@permissionService.canManageSession(#dto.id)") + public String updateSession(ExamLabPatchDTO dto) { + ls.updateSession(dto); + return "redirect:/lab/" + dto.getId(); } /** * Updates the given session entity using the given session patch DTO. The session in question is of the * limited capacity type. * - * @param session The session to update. - * @param dto The dto representing the changes to apply to the session. - * @return A redirect to the session view page. + * @param dto The dto representing the changes to apply to the session. + * @return A redirect to the session view page. */ @Transactional - @PostMapping("/lab/{session}/edit/capacity") - @PreAuthorize("@permissionService.canManageSession(#session)") - public String updateSession(@PathEntity CapacitySession session, CapacitySessionPatchDTO dto) { + @PatchMapping("/lab/capacity") + @PreAuthorize("@permissionService.canManageSession(#dto.id)") + public String updateSession(CapacitySessionPatchDTO dto) { + CapacitySession session = (CapacitySession) qsRepository.findByIdOrThrow(dto.getId()); SelectionProcedure oldProcedure = session.getCapacitySessionConfig().getProcedure(); - ls.updateSession(dto, session); + ls.updateSession(dto); ls.updateCapacitySessionRequests(session, oldProcedure); - return "redirect:/lab/" + session.getId(); + return "redirect:/lab/" + dto.getId(); } /** @@ -724,11 +645,11 @@ public class LabController { var divisions = modules.stream() .flatMap(m -> m.getDivisions().stream()).collect(Collectors.toList()); - var rooms = rCache.getAll(); + // var rooms = rCache.getAll(); modules.sort(Comparator.comparing(ModuleDetailsDTO::getName)); assignments.sort(Comparator.comparing(AssignmentDetailsDTO::getName)); - rooms.sort(Comparator.comparing((RoomDetailsDTO r) -> r.getBuilding().getName() + r.getName())); + // rooms.sort(Comparator.comparing((RoomDetailsDTO r) -> r.getBuilding().getName() + r.getName())); model.addAttribute("editions", editions); model.addAttribute("editionMap", @@ -741,11 +662,11 @@ public class LabController { model.addAttribute("clusters", clusters); model.addAttribute("divisions", divisions); - model.addAttribute("rooms", rooms); - model.addAttribute("buildings", rooms.stream() - .map(RoomDetailsDTO::getBuilding).distinct() - .sorted(Comparator.comparing(BuildingSummaryDTO::getName)) - .collect(Collectors.toList())); + // model.addAttribute("rooms", rooms); + // model.addAttribute("buildings", rooms.stream() + // .map(RoomDetailsDTO::getBuilding).distinct() + // .sorted(Comparator.comparing(BuildingSummaryDTO::getName)) + // .collect(Collectors.toList())); TODO } /** diff --git a/src/main/java/nl/tudelft/queue/controller/MailController.java b/src/main/java/nl/tudelft/queue/controller/MailController.java index 3f6f11cd5ed33e01df860aaf50b55adca6244b41..60477ea627d0d1940e3a40e70c686054d4bac541 100644 --- a/src/main/java/nl/tudelft/queue/controller/MailController.java +++ b/src/main/java/nl/tudelft/queue/controller/MailController.java @@ -17,8 +17,8 @@ */ package nl.tudelft.queue.controller; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; -import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.queue.dto.create.CourseRequestCreateDTO; import nl.tudelft.queue.service.MailService; @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Controller @ConditionalOnProperty(name = "queue.mail.enabled", havingValue = "true") @@ -44,11 +45,13 @@ public class MailController { */ @PostMapping("/editions/request-course") @PreAuthorize("@permissionService.isAdminOrTeacher()") - public String requestCourse(@AuthenticatedPerson Person person, - CourseRequestCreateDTO dto) { + public String requestCourse(@AuthenticatedPerson Person person, CourseRequestCreateDTO dto, + RedirectAttributes redirectAttributes) { dto.validateOrThrow(); ms.sendCourseRequest(person, dto); - return "course/request-submitted"; + redirectAttributes.addFlashAttribute("success", "Course successfully requested"); + + return "redirect:/"; } } diff --git a/src/main/java/nl/tudelft/queue/controller/ModuleController.java b/src/main/java/nl/tudelft/queue/controller/ModuleController.java index a86c0aed9e402b5c200de3660f1bbcaf1c33cf5c..6f661f19a3e339e163a406baf2875f5be79010e7 100644 --- a/src/main/java/nl/tudelft/queue/controller/ModuleController.java +++ b/src/main/java/nl/tudelft/queue/controller/ModuleController.java @@ -18,19 +18,18 @@ package nl.tudelft.queue.controller; import nl.tudelft.labracore.api.ModuleControllerApi; -import nl.tudelft.labracore.api.dto.EditionDetailsDTO; -import nl.tudelft.labracore.api.dto.ModuleCreateDTO; -import nl.tudelft.queue.cache.EditionCacheManager; -import nl.tudelft.queue.cache.ModuleCacheManager; +import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.labracore.lib.cache.EditionCacheManager; +import nl.tudelft.labracore.lib.cache.ModuleCacheManager; +import nl.tudelft.labracore.lib.data.Module; import nl.tudelft.queue.dto.create.QueueModuleCreateDTO; +import nl.tudelft.queue.dto.patch.QueueModulePatchDTO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.*; @Controller public class ModuleController { @@ -45,42 +44,55 @@ public class ModuleController { private ModuleControllerApi mApi; /** - * Gets the creation page for a module in the given edition. This sets up page attributes and returns the - * thymeleaf template to resolve. + * Gets the details of a module. * - * @param editionId The id of the edition the module should be in. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. + * @param id The id of the module + * @return The details of the module */ - @GetMapping("/edition/{editionId}/modules/create") - @PreAuthorize("@permissionService.canManageModules(#editionId)") - public String getModuleCreatePage(@PathVariable Long editionId, Model model) { - return addCreateModuleAttributes(editionId, new QueueModuleCreateDTO(editionId), model); + @GetMapping("/module/{id}") + public @ResponseBody ModuleDetailsDTO getModule(@PathVariable Long id) { + return Module.byId(id).getDto(); } /** * Creates a module from the given DTO in the edition with the given id. * - * @param editionId The id of the edition to create the module in. - * @param dto The creation DTO containing all information needed to create a new module. - * @param model The model to fill out for Thymeleaf template resolution. - * @return A redirect back to the modules page or the module create page if something goes - * wrong. + * @param dto The creation DTO containing all information needed to create a new module. + * @return A redirect back to the modules page or the module create page if something goes wrong. */ - @PostMapping("/edition/{editionId}/modules/create") - @PreAuthorize("@permissionService.canManageModules(#editionId)") - public String createModule(@PathVariable Long editionId, QueueModuleCreateDTO dto, Model model) { - // Ensure the DTO contains the right edition ID, as malicious users can inject a different ID in the form - dto.setEditionId(editionId); - + @PostMapping("/module") + @PreAuthorize("@permissionService.canManageModules(#dto.edition)") + public String createModule(QueueModuleCreateDTO dto) { ModuleCreateDTO create = dto.apply(); - if (dto.hasErrors()) { - return addCreateModuleAttributes(editionId, dto, model); - } - mApi.addModule(create).block(); + return "redirect:/edition/" + dto.getEdition() + "/modules"; + } - return "redirect:/edition/" + editionId + "/modules"; + /** + * Patches a module from the given DTO. + * + * @param dto The creation DTO containing all information needed to create a new module. + * @return A redirect back to the modules page or the module create page if something goes wrong. + */ + @PatchMapping("/module") + @PreAuthorize("@permissionService.canManageModule(#dto.module)") + public String editModule(QueueModulePatchDTO dto) { + ModulePatchDTO patch = dto.apply(); + mApi.patchModule(dto.getModule(), patch).block(); + return "redirect:/edition/" + Module.byId(dto.getModule()).getEdition().value().getId() + "/modules"; + } + + /** + * Removes the module with the given id. + * + * @param id The id of the module to delete. + * @return A redirect back to the edition modules page. + */ + @DeleteMapping("/module") + @PreAuthorize("@permissionService.canManageModule(#id)") + public String removeModule(@RequestParam Long id) { + mApi.deleteModule(id).block(); + return "redirect:/edition/" + Module.byId(id).getEdition().value().getId() + "/modules"; } /** @@ -104,23 +116,6 @@ public class ModuleController { return "module/remove"; } - /** - * Removes the module with the given id. - * - * @param moduleId The id of the module to delete. - * @return A redirect back to the edition modules page. - */ - @PostMapping("/module/{moduleId}/remove") - @PreAuthorize("@permissionService.canManageModule(#moduleId)") - public String removeModule(@PathVariable Long moduleId) { - var module = mCache.getOrThrow(moduleId); - var edition = eCache.getOrThrow(module.getEdition().getId()); - - mApi.deleteModule(moduleId).block(); - - return "redirect:/edition/" + edition.getId() + "/modules"; - } - /** * Fills in page attributes for the module creation page with the id of the edition and the initial module * creation DTO object. diff --git a/src/main/java/nl/tudelft/queue/controller/ParticipantController.java b/src/main/java/nl/tudelft/queue/controller/ParticipantController.java new file mode 100644 index 0000000000000000000000000000000000000000..d9af6fd21baf4d277ae2ebbf8a990726c87bb481 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/controller/ParticipantController.java @@ -0,0 +1,125 @@ +/* + * 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 java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import nl.tudelft.labracore.api.EditionControllerApi; +import nl.tudelft.labracore.api.PersonControllerApi; +import nl.tudelft.labracore.api.RoleControllerApi; +import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.queue.csv.EmptyCsvException; +import nl.tudelft.queue.csv.InvalidCsvException; +import nl.tudelft.queue.dto.create.QueueRoleCreateDTO; +import nl.tudelft.queue.dto.create.QueueRolesCreateDTO; +import nl.tudelft.queue.dto.patch.QueueRolePatchDTO; +import nl.tudelft.queue.service.EditionService; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +@Controller +@RequestMapping("/participant") +public class ParticipantController { + + @Autowired + private RoleControllerApi rApi; + @Autowired + private PersonControllerApi pApi; + @Autowired + private EditionControllerApi eApi; + + @Autowired + private EditionService es; + + @PostMapping("/single") + @PreAuthorize("@permissionService.canManageParticipants(#dto.edition)") + public String addSingleParticipant(QueueRoleCreateDTO dto, RedirectAttributes redirectAttributes) { + List<PersonSummaryDTO> person = pApi.searchForPeople(List.of(dto.getIdentifier())).block() + .getPeople(); + + if (person.isEmpty()) { + redirectAttributes.addFlashAttribute("error", + "User with username, email, or number " + dto.getIdentifier() + " cannot be found."); + } else if (!dto.hasErrors()) { + rApi.addRole(new RoleCreateDTO().person(new PersonIdDTO().id(person.get(0).getId())) + .edition(new EditionIdDTO().id(dto.getEdition())).type(dto.getRole())).block(); + } + + return "redirect:/edition/" + dto.getEdition() + "/participants"; + } + + @PostMapping("/multiple") + @PreAuthorize("@permissionService.canManageParticipants(#dto.edition)") + public String addMultipleParticipants(QueueRolesCreateDTO dto, RedirectAttributes redirectAttributes) { + PersonSearchDTO result = pApi + .searchForPeople(Arrays.stream(dto.getIdentifiers().split("\n")).map(String::strip).toList()) + .block(); + eApi.addParticipantsToEdition(dto.getEdition(), + new ParticipantsImportDTO().participants(result.getPeople().stream() + .collect(Collectors.toMap(PersonSummaryDTO::getUsername, __ -> dto.getRole()))), + false).block(); + + if (!result.getNotFound().isEmpty()) { + redirectAttributes.addFlashAttribute("error", + "The following users were not found: " + String.join(", ", result.getNotFound())); + } + + return "redirect:/edition/" + dto.getEdition() + "/participants"; + } + + @PostMapping("/import") + @PreAuthorize("@permissionService.canManageParticipants(#edition)") + public String importParticipants(@RequestParam Long edition, @RequestParam MultipartFile file, + RedirectAttributes redirectAttributes) { + try { + List<String> notFound = es.addCourseParticipants(file, edition); + if (!notFound.isEmpty()) { + redirectAttributes.addFlashAttribute("error", + "The following users were not found: " + String.join(", ", notFound)); + } + } catch (InvalidCsvException | EmptyCsvException e) { + redirectAttributes.addFlashAttribute("error", "Invalid CSV file"); + } + + return "redirect:/edition/" + edition + "/participants"; + } + + @PatchMapping + @PreAuthorize("@permissionService.canManageParticipant(#dto.edition, #dto.person)") + public String changeRole(QueueRolePatchDTO dto) { + if (!dto.hasErrors()) { + rApi.patchRole(dto.getPerson(), dto.getEdition(), new RolePatchDTO().type(dto.getRole())).block(); + } + return "redirect:/edition/" + dto.getEdition() + "/participants"; + } + + @DeleteMapping + @PreAuthorize("@permissionService.canManageParticipant(#edition, #person)") + public String changeRole(@RequestParam Long edition, @RequestParam Long person) { + eApi.removePersonFromEdition(edition, person).block(); + return "redirect:/edition/" + edition + "/participants"; + } + +} diff --git a/src/main/java/nl/tudelft/queue/controller/PushController.java b/src/main/java/nl/tudelft/queue/controller/PushController.java index ce9623852f587e374dda4a57112a9f8913321d94..6513f39ada8854e67f1be9bb6527df6d6d148f5b 100644 --- a/src/main/java/nl/tudelft/queue/controller/PushController.java +++ b/src/main/java/nl/tudelft/queue/controller/PushController.java @@ -19,8 +19,8 @@ package nl.tudelft.queue.controller; import nl.martijndwars.webpush.PushService; import nl.martijndwars.webpush.Utils; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; -import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.queue.dto.create.SubscriptionCreateDTO; import nl.tudelft.queue.realtime.SubscriptionContainer; diff --git a/src/main/java/nl/tudelft/queue/controller/QuestionController.java b/src/main/java/nl/tudelft/queue/controller/QuestionController.java new file mode 100644 index 0000000000000000000000000000000000000000..a05be45b89296d7b57665a60fe9c7b3a2e721f7d --- /dev/null +++ b/src/main/java/nl/tudelft/queue/controller/QuestionController.java @@ -0,0 +1,76 @@ +/* + * 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 nl.tudelft.labracore.api.QuestionControllerApi; +import nl.tudelft.labracore.api.dto.QuestionCreateDTO; +import nl.tudelft.labracore.api.dto.QuestionPatchDTO; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +@Controller +@RequestMapping("/question") +public class QuestionController { + + @Autowired + private QuestionControllerApi qApi; + + /** + * Adds a question. + * + * @param question The question to add + * @return The question page of the edition + */ + @PostMapping + @PreAuthorize("@permissionService.canManageQuestions(#question.edition.id)") + public String addQuestion(QuestionCreateDTO question) { + qApi.addQuestion(question).block(); + return "/edition/" + question.getEdition().getId() + "/questions"; + } + + /** + * Edits a question. + * + * @param questionId The id of the question to edit + * @param question The new question + * @return The question page of the edition + */ + @PatchMapping + @PreAuthorize("@permissionService.canManageQuestions(#question.edition.id)") + public String editQuestion(@RequestParam Long questionId, QuestionPatchDTO question) { + qApi.patchQuestion(questionId, question).block(); + return "/edition/" + question.getEdition().getId() + "/questions"; + } + + /** + * Deletes a question. + * + * @param questionId The id of the question to edit + * @return The question page of the edition + */ + @DeleteMapping + @PreAuthorize("@permissionService.canManageQuestions(#editionId)") + public String editQuestion(@RequestParam Long editionId, @RequestParam Long questionId) { + qApi.deleteQuestion(questionId).block(); + return "/edition/" + editionId + "/questions"; + } + +} diff --git a/src/main/java/nl/tudelft/queue/controller/RequestController.java b/src/main/java/nl/tudelft/queue/controller/RequestController.java index 38e8139c8a436f192232db7189b145762b854f45..1b4b3ce0e319459831921018a9892b51aa403e6a 100644 --- a/src/main/java/nl/tudelft/queue/controller/RequestController.java +++ b/src/main/java/nl/tudelft/queue/controller/RequestController.java @@ -21,18 +21,16 @@ import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import javax.transaction.Transactional; import nl.tudelft.labracore.api.PersonControllerApi; -import nl.tudelft.labracore.api.dto.PersonSummaryDTO; +import nl.tudelft.labracore.lib.cache.*; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; -import nl.tudelft.labracore.lib.security.user.Person; import nl.tudelft.librador.dto.view.View; import nl.tudelft.librador.resolver.annotations.PathEntity; -import nl.tudelft.queue.cache.*; import nl.tudelft.queue.dto.patch.FeedbackPatchDTO; import nl.tudelft.queue.dto.patch.RequestPatchDTO; import nl.tudelft.queue.dto.util.RequestTableFilterDTO; @@ -43,17 +41,18 @@ import nl.tudelft.queue.model.Request; import nl.tudelft.queue.model.SelectionRequest; import nl.tudelft.queue.model.labs.Lab; import nl.tudelft.queue.repository.LabRequestRepository; +import nl.tudelft.queue.repository.RequestRepository; import nl.tudelft.queue.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; 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.servlet.mvc.support.RedirectAttributes; @Controller public class RequestController { @@ -61,6 +60,9 @@ public class RequestController { @Autowired private LabRequestRepository lrr; + @Autowired + private RequestRepository rr; + @Autowired private RequestService rs; @@ -146,10 +148,14 @@ public class RequestController { model.addAttribute("page", "requests"); model.addAttribute("filter", filter); - model.addAttribute("requests", rts.convertRequestsToView( - lrr.findAllByFilter(labs, filter, pageable), lrr.countByFilter(labs, filter))); - model.addAttribute("requestCounts", rts.labRequestCounts( - labs, assistant, filter)); + model.addAttribute("requests", + rts.convertRequestsToView(lrr.findAllByFilterForPerson(labs, filter, assistant, pageable), + lrr.countByFilterForPerson(labs, assistant, filter))); + model.addAttribute("currentRequests", + labs.stream().map(lab -> lrr.findCurrentlyProcessingRequest(assistant, lab)) + .filter(Optional::isPresent).map(Optional::get).map(LabRequest::toViewDTO) + .collect(Collectors.toList())); + model.addAttribute("requestCounts", rts.labRequestCounts(labs, assistant, filter)); return "request/list"; } @@ -183,18 +189,17 @@ public class RequestController { /** * Updates the information on the location of a student within the given request. * - * @param request The request to update location information on. - * @param dto The dto carrying the update information on the location. - * @return A redirect back to the lab view page. + * @param request The request to update location information on. + * @param dto The dto carrying the update information on the location. */ @Transactional @PostMapping("/request/{request}/update-request-info") @PreAuthorize("@permissionService.canUpdateRequest(#request.id)") - public String updateRequestInfo(@PathEntity LabRequest request, + public ResponseEntity<Void> updateRequestInfo(@PathEntity LabRequest request, RequestPatchDTO dto) { dto.apply(request); - return "redirect:/lab/" + request.getSession().getId(); + return ResponseEntity.ok().build(); } /** @@ -225,82 +230,46 @@ public class RequestController { } } - /** - * Gets the request approval view. This page is used to submit a request approval. - * - * @param request The request that is requested to be approved. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/request/{request}/approve") - @PreAuthorize("@permissionService.canFinishRequest(#request.id)") - public String getRequestApproveView(@PathEntity LabRequest request, Model model) { - model.addAttribute("request", request); - ls.setOrganizationInModel(request.getSession(), model); - - return "request/approve"; - } - /** * Performs the 'approve request' action for a request. This controller method calls the service method to * handle a request approve command and redirects the user back to the requests page to grab their next * request. * - * @param request The request that the user approves. + * @param requestId The id of the request that the user approves. * @param person The user that is approving the given request. * @param reasonForAssistant The reason the assistant gives other assistants for approving the request. - * @param redirectAttributes Attributes used to fill in a model message upon redirection. * @return A redirect to the requests table page. */ - @PostMapping("/request/{request}/approve") - @PreAuthorize("@permissionService.canFinishRequest(#request.id)") - public String approveRequest(@PathEntity LabRequest request, - @AuthenticatedPerson Person person, + @PostMapping("/request/approve") + @PreAuthorize("@permissionService.canFinishRequest(#requestId)") + public String approveRequest(@AuthenticatedPerson Person person, + @RequestParam(value = "request") Long requestId, @RequestParam(value = "reasonForAssistant") String reasonForAssistant, - @RequestParam(value = "answer", required = false) String answer, - RedirectAttributes redirectAttributes) { + @RequestParam(value = "answer", required = false) String answer) { + LabRequest request = lrr.findByIdOrThrow(requestId); rs.approveRequest(request, person.getId(), reasonForAssistant, answer); - redirectAttributes.addFlashAttribute("message", "Request #" + request.getId() + " approved"); - return "redirect:/requests"; } - /** - * Gets the request reject view. This page is used to submit a request rejection. - * - * @param request The request that is requested to be rejected. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/request/{request}/reject") - @PreAuthorize("@permissionService.canFinishRequest(#request.id)") - public String getRequestRejectView(@PathEntity Request<?> request, Model model) { - model.addAttribute("request", request); - ls.setOrganizationInModel(request.getSession(), model); - - return "request/reject"; - } - /** * Performs the 'reject request' action for a request. This controller method calls the service method to * handle a request reject command and redirects the user back to the requests page to grab their next * request. * - * @param request The request that the user rejects. + * @param requestId The id of the request that the user rejects. * @param person The user that is rejecting the given request. * @param reasonForAssistant The reason the assistant gives other assistants for rejecting the request. * @param reasonForStudent The reason the assistant gives the student(s) for rejecting the request. - * @param redirectAttributes Attributes used to fill in a model message upon redirection. * @return A redirect to the requests table page. */ - @PostMapping("/request/{request}/reject") - @PreAuthorize("@permissionService.canFinishRequest(#request.id)") - public String rejectRequest(@PathEntity Request<?> request, - @AuthenticatedPerson Person person, + @PostMapping("/request/reject") + @PreAuthorize("@permissionService.canFinishRequest(#requestId)") + public String rejectRequest(@AuthenticatedPerson Person person, + @RequestParam(value = "request") Long requestId, @RequestParam(value = "reasonForAssistant") String reasonForAssistant, - @RequestParam(value = "reasonForStudent") String reasonForStudent, - RedirectAttributes redirectAttributes) { + @RequestParam(value = "reasonForStudent") String reasonForStudent) { + Request<?> request = rr.findByIdOrThrow(requestId); if (request instanceof LabRequest) { rs.rejectRequest((LabRequest) request, person.getId(), reasonForAssistant, reasonForStudent); } else { @@ -308,63 +277,32 @@ public class RequestController { reasonForAssistant, reasonForStudent); } - redirectAttributes.addFlashAttribute("message", "Request #" + request.getId() + " rejected"); - return "redirect:/requests"; } - /** - * Gets the request forward view. This page is used to forward a request to a different TA. - * - * @param request The request that is requested to be forwarded. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ - @GetMapping("/request/{request}/forward") - @PreAuthorize("@permissionService.canFinishRequest(#request.id)") - public String getRequestForwardView(@AuthenticatedPerson Person user, - @PathEntity LabRequest request, Model model) { - List<PersonSummaryDTO> assistants = rds - .roles(eCache.getOrThrow( - mCache.getOrThrow(aCache.getOrThrow(request.getAssignment()).getModule().getId()) - .getEdition().getId()), - Set.of(TA, HEAD_TA, TEACHER)) - .stream().filter(p -> !p.getId().equals(user.getId())).toList(); - - ls.setOrganizationInModel(request.getSession(), model); - - model.addAttribute("request", request); - model.addAttribute("assistants", assistants); - - return "request/forward"; - } - /** * Performs the 'forward request' action for a request. This controller method calls the service method to * handle a request forward command and redirects the user back to the requests page to grab their next * request. * - * @param request The request that the user forwards. + * @param requestId The id of the request that the user forwards. * @param assistant The assistant the request should be forwarded to. * @param reasonForAssistant The reason the assistant gives other assistants for forwarding the request. - * @param redirectAttributes Attributes used to fill in a model message upon redirection. * @return A redirect to the requests table page. */ - @PostMapping("/request/{request}/forward") - @PreAuthorize("@permissionService.canFinishRequest(#request.id)") - public String forwardRequest(@PathEntity LabRequest request, - @AuthenticatedPerson Person assistant, + @PostMapping("/request/forward") + @PreAuthorize("@permissionService.canFinishRequest(#requestId)") + public String forwardRequest(@AuthenticatedPerson Person assistant, + @RequestParam(value = "request") Long requestId, @RequestParam(value = "assistant") Long forwardedTo, - @RequestParam(value = "reasonForAssistant") String reasonForAssistant, - RedirectAttributes redirectAttributes) { + @RequestParam(value = "reasonForAssistant") String reasonForAssistant) { + LabRequest request = lrr.findByIdOrThrow(requestId); if (forwardedTo == -1L) { rs.forwardRequestToAnyone(request, assistant, reasonForAssistant); } else { rs.forwardRequestToPerson(request, assistant, pCache.getOrThrow(forwardedTo), reasonForAssistant); } - redirectAttributes.addFlashAttribute("message", "Request #" + request.getId() + " forwarded"); - return "redirect:/requests"; } @@ -393,7 +331,7 @@ public class RequestController { * @return The request the user is currently processing. Either the picked request, or the request * he/she was still processing. */ - @GetMapping("/request/{request}/pick") + @PostMapping("/request/{request}/pick") @PreAuthorize("@permissionService.canPickRequest(#request.id)") public String pickRequest(@PathEntity LabRequest request, @AuthenticatedPerson Person person) { LabRequest currRequest = rs.pickRequest(person, request); @@ -414,7 +352,7 @@ public class RequestController { FeedbackPatchDTO dto) { fs.updateFeedback(request, assistantId, dto); - return "redirect:/request/" + request.getId(); + return "redirect:/lab/" + request.getSession().getId(); } } diff --git a/src/main/java/nl/tudelft/queue/controller/RequestFilterController.java b/src/main/java/nl/tudelft/queue/controller/RequestFilterController.java index 3f8f6a728f65cd3a47cbb0d5e61bbfaf980ee4bc..37ce7c5b2f0921af1b0577f106c61c12971b09c4 100644 --- a/src/main/java/nl/tudelft/queue/controller/RequestFilterController.java +++ b/src/main/java/nl/tudelft/queue/controller/RequestFilterController.java @@ -18,6 +18,7 @@ package nl.tudelft.queue.controller; import nl.tudelft.queue.dto.util.RequestTableFilterDTO; +import nl.tudelft.queue.dto.util.RequestTableFilterUpdateDTO; import nl.tudelft.queue.service.RequestTableService; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +52,27 @@ public class RequestFilterController { return "redirect:" + path; } + /** + * Updates filters for a request table page with a return-to-page path. + * + * @param update The DTO containing information about the filters that are to be updated. + * @param path The path this method should redirect to after submitting filters. + * @param redirectAttributes Attributes used to fill in a model message upon redirection. + * @return A redirect to the associated requests table page. + */ + @PostMapping(value = "/filter", params = "filter-update") + public String updateRequestTableFilters(RequestTableFilterUpdateDTO update, + @RequestParam("return-path") String path, + RedirectAttributes redirectAttributes) { + RequestTableFilterDTO filter = rts.checkAndStoreFilterDTO(null, path); + filter = update.updateFilter(filter); + rts.checkAndStoreFilterDTO(filter, path); + + redirectAttributes.addAttribute("page", 0); + + return "redirect:" + path; + } + /** * Clears filters for a request table under the given return-to-page path. * diff --git a/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java b/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java index b1fa235f51bff95ac358d54dfe1848cdc3dd7eb4..b9c04ed11296b608b45dcac301dfe94af8f979f2 100644 --- a/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java +++ b/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java @@ -18,23 +18,33 @@ package nl.tudelft.queue.controller; import java.time.LocalDateTime; +import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.validation.Valid; -import nl.tudelft.labracore.api.EditionCollectionControllerApi; -import nl.tudelft.labracore.api.RoleControllerApi; +import nl.tudelft.labracore.api.*; import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.labracore.lib.api.RoleType; +import nl.tudelft.labracore.lib.cache.EditionCacheManager; +import nl.tudelft.labracore.lib.cache.EditionCollectionCacheManager; +import nl.tudelft.labracore.lib.cache.ModuleCacheManager; +import nl.tudelft.labracore.lib.data.Edition; +import nl.tudelft.labracore.lib.data.EditionCollection; +import nl.tudelft.labracore.lib.data.Module; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.EditionCacheManager; -import nl.tudelft.queue.cache.EditionCollectionCacheManager; +import nl.tudelft.queue.PageUtil; import nl.tudelft.queue.dto.create.QueueEditionCollectionCreateDTO; import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO; import nl.tudelft.queue.repository.LabRepository; +import nl.tudelft.queue.service.EditionService; import nl.tudelft.queue.service.RoleDTOService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -53,41 +63,54 @@ public class SharedEditionController { @Autowired private EditionCacheManager editionCacheManager; + @Autowired + private EditionCacheManager eCache; + + @Autowired + private ModuleCacheManager mCache; + @Autowired private RoleDTOService roleDTOService; + @Autowired + private EditionService es; + @Autowired private LabRepository lr; @Autowired private RoleControllerApi rApi; - /** - * Get the page for a shared edition which includes all the details of the specific shared edition. - * - * @param id The id of the shared edition to be displayed. - * @param model The model to fill out for Thymeleaf template resolution. - * @return The Thymeleaf template to resolve. - */ + @Autowired + private CourseControllerApi cApi; + + @Autowired + private CohortControllerApi cohortApi; + + @Autowired + private BuildingControllerApi bApi; + @GetMapping("/{id}") @PreAuthorize("@permissionService.isAuthenticated()") - public String getSharedEdition(@PathVariable Long id, Model model) { + public String getSharedEdition(@PathVariable Long id) { + EditionCollection collection = EditionCollection.byId(id); + List<Module> modules = Module.byIds(collection.getEditions().get().stream() + .flatMap(e -> e.getModules().value().stream()) + .map(Module::getId).toList()); + boolean hasAssignments = modules.stream().anyMatch(m -> !m.getAssignments().value().isEmpty()); + if (hasAssignments) { + return "redirect:/shared-edition/{id}/labs"; + } + return "redirect:/shared-edition/{id}/editions"; + } + + @GetMapping("/{id}/labs") + @PreAuthorize("@permissionService.isAuthenticated()") + public String getSharedEditionLabs(@PathVariable Long id, Model model) { var collection = editionCollectionCacheManager.getOrThrow(id); var editions = editionCacheManager.get( collection.getEditions().stream().map(EditionSummaryDTO::getId).toList()); - var teachers = editions.stream().map(edition -> roleDTOService.teachers(edition)) - .flatMap(List::stream).toList(); - - var headTas = editions.stream().map(edition -> roleDTOService.headTAs(edition)).flatMap(List::stream) - .toList(); - - var assistants = editions.stream().map(edition -> roleDTOService.assistants(edition)) - .flatMap(List::stream).toList(); - - var students = editions.stream().map(edition -> roleDTOService.students(edition)) - .flatMap(List::stream).toList(); - var allSessions = editions.stream().map(EditionDetailsDTO::getSessions).flatMap(List::stream) .toList(); @@ -106,22 +129,85 @@ public class SharedEditionController { var pastSessions = lr.findAllBySessions(pastSessionsIds).stream() .map(l -> View.convert(l, QueueSessionSummaryDTO.class)).toList(); - var editionTeachers = editions.stream().collect(Collectors.toMap(EditionDetailsDTO::getId, - s -> roleDTOService.teacherNames(s))); + model.addAttribute("collection", collection); + model.addAttribute("currentSessions", currentSessions); + model.addAttribute("upcomingSessions", upComingSessions); + model.addAttribute("pastSessions", pastSessions); + model.addAttribute("allModules", editions.stream().flatMap(e -> e.getModules().stream()).toList()); + + model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block()); + model.addAttribute("assignments", + mCache.get( + editions.stream().flatMap(e -> e.getModules().stream()).map(ModuleSummaryDTO::getId)) + .stream().flatMap(m -> m.getAssignments().stream()).toList()); + model.addAttribute("buildings", bApi.getAllBuildings().collectList().block()); + model.addAttribute("clusters", Edition.byIds(editions.stream().map(EditionDetailsDTO::getId).toList()) + .stream().flatMap(e -> e.getCohort().get().getClusters().value().stream()).toList()); + + return "shared-edition/view/labs"; + } + + @GetMapping("/{id}/participants") + @PreAuthorize("@permissionService.isAuthenticated()") + public String getSharedEditionParticipants(@PathVariable Long id, Model model, + @RequestParam(value = "search", required = false) String search, + @RequestParam(value = "roles", required = false) List<RolePersonDetailsDTO.TypeEnum> roleFilter, + @PageableDefault(size = 25) Pageable pageable) { + fillParticipantsModel(id, model, search, roleFilter, pageable); + return "shared-edition/view/participants"; + } + + @GetMapping("/{id}/participants/list") + @PreAuthorize("@permissionService.isAuthenticated()") + public String getSharedEditionParticipantsListOnly(@PathVariable Long id, Model model, + @RequestParam(value = "search", required = false) String search, + @RequestParam(value = "roles", required = false) List<RolePersonDetailsDTO.TypeEnum> roleFilter, + @PageableDefault(size = 25) Pageable pageable) { + fillParticipantsModel(id, model, search, roleFilter, pageable); + return "shared-edition/view/participants_table"; + } - var roles = rApi.getRolesByEditions(editions.stream().map(EditionDetailsDTO::getId).toList()) - .filter(r -> r.getType() != RolePersonDetailsDTO.TypeEnum.STUDENT) + private void fillParticipantsModel(Long id, Model model, String search, + List<RolePersonDetailsDTO.TypeEnum> roleFilter, Pageable pageable) { + var collection = editionCollectionCacheManager.getOrThrow(id); + var editions = editionCacheManager.get( + collection.getEditions().stream().map(EditionSummaryDTO::getId).toList()); + + var rolesMap = rApi.getRolesByEditions(editions.stream().map(EditionDetailsDTO::getId).toList()) .collect(Collectors.groupingBy(RolePersonDetailsDTO::getPerson)).block(); + var roles = rolesMap.entrySet().stream() + .map(e -> search == null ? e + : Map.entry(e.getKey(), es.rolesMatchingFilter(e.getValue(), search))) + .map(e -> roleFilter == null || roleFilter.isEmpty() ? e + : Map.entry(e.getKey(), + e.getValue().stream().filter(r -> roleFilter.contains(r.getType())).toList())) + .filter(e -> !e.getValue().isEmpty()) + .sorted(Comparator.comparing(e -> e.getKey().getDisplayName())) + .sorted(Comparator.<Map.Entry<PersonSummaryDTO, List<RolePersonDetailsDTO>>>comparingInt( + e -> e.getValue().stream().map(RolePersonDetailsDTO::getType) + .max(Comparator.comparingInt(Enum::ordinal)).get().ordinal()) + .reversed()) + .toList(); model.addAttribute("collection", collection); model.addAttribute("editions", editions); - model.addAttribute("editionTeachers", editionTeachers); - model.addAttribute("roles", roles); - model.addAttribute("currentSessions", currentSessions); - model.addAttribute("upComingSessions", upComingSessions); - model.addAttribute("pastSessions", pastSessions); + model.addAttribute("roles", PageUtil.toPage(pageable, roles)); + } + + @GetMapping("/{id}/editions") + @PreAuthorize("@permissionService.isAuthenticated()") + public String getSharedEditionEditions(@PathVariable Long id, Model model) { + EditionCollection collection = EditionCollection.byId(id); + List<Edition> editions = collection.getEditions().get(); + + model.addAttribute("collection", collection); + model.addAttribute("editions", editions); + model.addAttribute("teachers", editions.stream() + .map(e -> Map.entry(e.getId(), + e.getRoles().get().stream().filter(r -> r.getType() == RoleType.TEACHER).toList())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); - return "shared-edition/view"; + return "shared-edition/view/editions"; } /** @@ -131,7 +217,7 @@ public class SharedEditionController { * @param sharedEdition The dto containing the shared edition to be created. * @return Redirect to the shared editions page. */ - @PostMapping("/create") + @PostMapping @PreAuthorize("@permissionService.isAdminOrTeacher()") public String createSharedEdition( @Valid QueueEditionCollectionCreateDTO sharedEdition) { diff --git a/src/main/java/nl/tudelft/queue/controller/TimeSlotController.java b/src/main/java/nl/tudelft/queue/controller/TimeSlotController.java index 5a479224b006599e92f1582a6c258c824b205d9a..103b9a589feb1606da6da05d4bfabe7e1cddb77d 100644 --- a/src/main/java/nl/tudelft/queue/controller/TimeSlotController.java +++ b/src/main/java/nl/tudelft/queue/controller/TimeSlotController.java @@ -21,8 +21,8 @@ import java.util.Optional; import javax.transaction.Transactional; +import nl.tudelft.labracore.lib.data.Person; 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.model.ClosableTimeSlot; import nl.tudelft.queue.model.LabRequest; @@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; @Controller public class TimeSlotController { @@ -51,7 +52,7 @@ public class TimeSlotController { * @return A redirect to the lab overview page. */ @Transactional - @GetMapping("/time-slot/{timeSlot}/close") + @PostMapping("/time-slot/{timeSlot}/close") @PreAuthorize("@permissionService.canCloseTimeSlot(#timeSlot)") public String closeTimeSlot(@PathEntity ClosableTimeSlot timeSlot) { tss.closeTimeSlot(timeSlot); diff --git a/src/main/java/nl/tudelft/queue/csv/UserCsvHelper.java b/src/main/java/nl/tudelft/queue/csv/UserCsvHelper.java index f09111678093f9eb141098617ffd06f5a2ab9448..f27d94c1e1cfd4c3dda81ff170b881af633e088a 100644 --- a/src/main/java/nl/tudelft/queue/csv/UserCsvHelper.java +++ b/src/main/java/nl/tudelft/queue/csv/UserCsvHelper.java @@ -20,7 +20,7 @@ package nl.tudelft.queue.csv; import java.util.List; import lombok.Getter; -import nl.tudelft.labracore.api.dto.RoleCreateDTO; +import nl.tudelft.labracore.api.dto.ParticipantsImportDTO; import org.springframework.web.multipart.MultipartFile; @@ -29,27 +29,30 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.dataformat.csv.CsvSchema; @Getter -public class UserCsvHelper extends RoleCreateDTO { +public class UserCsvHelper { - private String netId; + private String identifier; + private ParticipantsImportDTO.InnerEnum role; private static final CsvSchema schema = CsvSchema.builder() - .setSkipFirstDataRow(true) - .addColumn("netId") + .setSkipFirstDataRow(false) + .addColumn("identifier") .addColumn("role") .setColumnSeparator(',') .build(); @JsonCreator - public UserCsvHelper(@JsonProperty("netId") String netId, @JsonProperty("role") String role) + public UserCsvHelper(@JsonProperty("identifier") String identifier, @JsonProperty("role") String role) throws IllegalArgumentException, InvalidCsvException { super(); - TypeEnum type = TypeEnum.fromValue(role.replace(" ", "").replace("\"", "")); - if (type.equals(TypeEnum.ADMIN) || type.equals(TypeEnum.BLOCKED)) { + ParticipantsImportDTO.InnerEnum type = ParticipantsImportDTO.InnerEnum + .fromValue(role.replace(" ", "").replace("\"", "")); + if (type.equals(ParticipantsImportDTO.InnerEnum.ADMIN) + || type.equals(ParticipantsImportDTO.InnerEnum.BLOCKED)) { throw new InvalidCsvException("Admins or blocked people cannot be added this way."); } - super.type(type); - this.netId = netId.replace(" ", "").replace("\"", ""); + this.identifier = identifier.replace(" ", "").replace("\"", ""); + this.role = type; } /** diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java index 7f329e5ad765c9e5017853c497e3e6541d5adaba..af5b27f592b64e558647a22e5c2783b9fd5e9f96 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java @@ -36,7 +36,7 @@ import org.springframework.format.annotation.DateTimeFormat; @AllArgsConstructor @EqualsAndHashCode(callSuper = false) public class QueueAssignmentCreateDTO extends Create<AssignmentCreateDTO> { - private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm"; + private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm"; @NotBlank private String name; @@ -48,10 +48,10 @@ public class QueueAssignmentCreateDTO extends Create<AssignmentCreateDTO> { private LocalDateTime deadline; @NotNull - private Long moduleId; + private Long module; - public QueueAssignmentCreateDTO(Long moduleId) { - this.moduleId = moduleId; + public QueueAssignmentCreateDTO(Long module) { + this.module = module; } @Override @@ -62,6 +62,6 @@ public class QueueAssignmentCreateDTO extends Create<AssignmentCreateDTO> { @Override protected void postApply(AssignmentCreateDTO data) { data.description(new Description().text(description)); - data.module(new ModuleIdDTO().id(moduleId)); + data.module(new ModuleIdDTO().id(module)); } } diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueEditionCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueEditionCreateDTO.java index 7b64c118a35f0d25c1d61e2b4e356da655eea81a..23afbbdd35f3414103c0e4950141626e5d653ab1 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/QueueEditionCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/QueueEditionCreateDTO.java @@ -36,7 +36,7 @@ import org.springframework.format.annotation.DateTimeFormat; @AllArgsConstructor @EqualsAndHashCode(callSuper = false) public class QueueEditionCreateDTO extends Create<EditionCreateDTO> { - private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm"; + private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm"; @NotBlank private String name; diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java index bc56b13233759aa4328c9e3e14a30ab24e015a25..7ca6cc10d9a49b5bddaf641e65314c58c3e780aa 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java @@ -30,13 +30,13 @@ import nl.tudelft.librador.dto.create.Create; @AllArgsConstructor @EqualsAndHashCode(callSuper = false) public class QueueModuleCreateDTO extends Create<ModuleCreateDTO> { - private Long editionId; + private Long edition; @NotBlank private String name; - public QueueModuleCreateDTO(Long editionId) { - this.editionId = editionId; + public QueueModuleCreateDTO(Long edition) { + this.edition = edition; } @Override @@ -46,6 +46,6 @@ public class QueueModuleCreateDTO extends Create<ModuleCreateDTO> { @Override protected void postApply(ModuleCreateDTO data) { - data.edition(new EditionIdDTO().id(editionId)); + data.edition(new EditionIdDTO().id(edition)); } } diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueRoleCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueRoleCreateDTO.java index 29a0e4409073e608f69db45f60b127c87438a32a..69ee65fe7160b0181d9f4f726e5770a76fb1dc50 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/QueueRoleCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/QueueRoleCreateDTO.java @@ -22,38 +22,28 @@ import static nl.tudelft.labracore.api.dto.RoleCreateDTO.TypeEnum.*; import java.util.Set; import lombok.*; -import nl.tudelft.labracore.api.dto.EditionIdDTO; import nl.tudelft.labracore.api.dto.RoleCreateDTO; -import nl.tudelft.librador.dto.create.Create; +import nl.tudelft.librador.dto.Validated; @Data @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = false) -public class QueueRoleCreateDTO extends Create<RoleCreateDTO> { - private Long editionId; - private String username; - private RoleCreateDTO.TypeEnum type; +public class QueueRoleCreateDTO extends Validated { + private Long edition; + private String identifier; + private RoleCreateDTO.TypeEnum role; - public QueueRoleCreateDTO(Long editionId) { - this.editionId = editionId; - } - - @Override - public Class<nl.tudelft.labracore.api.dto.RoleCreateDTO> clazz() { - return RoleCreateDTO.class; + public QueueRoleCreateDTO(Long edition) { + this.edition = edition; } @Override public void validate() { - if (Set.of(ADMIN, TEACHER_RO, BLOCKED).contains(type)) { + if (Set.of(ADMIN, TEACHER_RO, BLOCKED).contains(role)) { errors.rejectValue("type", "The 'type' field for a role cannot be ADMIN, BLOCKED or TEACHER_RO"); } } - @Override - protected void postApply(RoleCreateDTO data) { - data.edition(new EditionIdDTO().id(editionId)); - } } diff --git a/src/main/java/nl/tudelft/queue/cache/SessionCacheManager.java b/src/main/java/nl/tudelft/queue/dto/create/QueueRolesCreateDTO.java similarity index 53% rename from src/main/java/nl/tudelft/queue/cache/SessionCacheManager.java rename to src/main/java/nl/tudelft/queue/dto/create/QueueRolesCreateDTO.java index d6ca827405103f8a97d708f4780afbc512748628..003a12e857271598021edd31ea533d537a7e165f 100644 --- a/src/main/java/nl/tudelft/queue/cache/SessionCacheManager.java +++ b/src/main/java/nl/tudelft/queue/dto/create/QueueRolesCreateDTO.java @@ -15,34 +15,35 @@ * 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.cache; +package nl.tudelft.queue.dto.create; -import java.util.List; +import static nl.tudelft.labracore.api.dto.ParticipantsImportDTO.InnerEnum.*; -import lombok.AllArgsConstructor; -import lombok.NoArgsConstructor; -import nl.tudelft.labracore.api.SessionControllerApi; -import nl.tudelft.labracore.api.dto.SessionDetailsDTO; +import java.util.Set; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; +import lombok.*; +import nl.tudelft.labracore.api.dto.ParticipantsImportDTO; +import nl.tudelft.librador.dto.Validated; -@Component -@RequestScope +@Data +@Builder @NoArgsConstructor @AllArgsConstructor -public class SessionCacheManager extends CoreCacheManager<Long, SessionDetailsDTO> { - @Autowired - private SessionControllerApi api; +@EqualsAndHashCode(callSuper = false) +public class QueueRolesCreateDTO extends Validated { + private Long edition; + private String identifiers; + private ParticipantsImportDTO.InnerEnum role; - @Override - protected List<SessionDetailsDTO> fetch(List<Long> ids) { - return api.getSessionsById(ids).collectList().block(); + public QueueRolesCreateDTO(Long edition) { + this.edition = edition; } @Override - protected Long getId(SessionDetailsDTO dto) { - return dto.getId(); + public void validate() { + if (Set.of(ADMIN, TEACHER_RO, BLOCKED).contains(role)) { + errors.rejectValue("type", "The 'type' field for a role cannot be ADMIN, BLOCKED or TEACHER_RO"); + } } + } diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java index 2ac52e1a5efd9ef78d0c80b4ddbeaaa5af6beaef..6536043b8c58dc91683932401f50ac0642d18645 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java @@ -25,8 +25,8 @@ import java.util.stream.Collectors; import lombok.*; import lombok.experimental.SuperBuilder; import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.labracore.lib.cache.RoomCacheManager; import nl.tudelft.librador.dto.create.Create; -import nl.tudelft.queue.cache.RoomCacheManager; import nl.tudelft.queue.dto.create.embeddables.LabRequestConstraintsCreateDTO; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.embeddables.Slot; @@ -37,6 +37,8 @@ import nl.tudelft.queue.model.embeddables.Slot; @AllArgsConstructor @EqualsAndHashCode(callSuper = false) public abstract class QueueSessionCreateDTO<D extends QueueSession<?>> extends Create<D> { + private Long organisationalUnit; + private String name; private Slot slot; diff --git a/src/main/java/nl/tudelft/queue/dto/create/embeddables/SlottedLabConfigCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/embeddables/SlottedLabConfigCreateDTO.java index ca01ba2a216f5d32e368d4044824fd89faac2282..68b266dfa27c0d7565ca4e541ee49edb03464ea6 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/embeddables/SlottedLabConfigCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/embeddables/SlottedLabConfigCreateDTO.java @@ -34,7 +34,7 @@ import org.springframework.format.annotation.DateTimeFormat; @AllArgsConstructor @EqualsAndHashCode(callSuper = false) public class SlottedLabConfigCreateDTO extends Create<SlottedLabConfig> { - private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm"; + private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm"; /** * The time that should be assigned to each of the timeslots. The slot duration is stored in minutes. diff --git a/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java index 1845586e73a64f611464be03d3439474d0cecb26..8b77a7d5fee234abb60987d85922b2eb460e50c5 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java @@ -41,6 +41,7 @@ import nl.tudelft.queue.model.labs.Lab; @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) public abstract class LabCreateDTO<D extends Lab> extends QueueSessionCreateDTO<D> { private CommunicationMethod communicationMethod; diff --git a/src/main/java/nl/tudelft/queue/dto/create/labs/RegularLabCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/labs/RegularLabCreateDTO.java index e7bd45e6e3f63b16cfb893d84e9aea92573c0cdd..0a7e1daf261a1d0a1cf8d19e433e8d5fa3cbf879 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/labs/RegularLabCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/labs/RegularLabCreateDTO.java @@ -26,6 +26,7 @@ import nl.tudelft.queue.model.labs.RegularLab; @SuperBuilder @AllArgsConstructor @EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) public class RegularLabCreateDTO extends LabCreateDTO<RegularLab> { public RegularLabCreateDTO(SessionDetailsDTO session, diff --git a/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java index f09d6ff1c1d6e92e09b66be4ded7cf3d84eafd16..4fbfd12d629cee5cb0cd68a5a5607670f3949ab3 100644 --- a/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java @@ -29,8 +29,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import nl.tudelft.queue.cache.AssignmentCacheManager; -import nl.tudelft.queue.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.cache.AssignmentCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; import nl.tudelft.queue.dto.create.RequestCreateDTO; import nl.tudelft.queue.dto.id.TimeSlotIdDTO; import nl.tudelft.queue.model.LabRequest; diff --git a/src/main/java/nl/tudelft/queue/dto/patch/QueueAssignmentPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueAssignmentPatchDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f4822dd3f1f67eae7ce4ab8cbe3364b818f4f2f --- /dev/null +++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueAssignmentPatchDTO.java @@ -0,0 +1,61 @@ +/* + * 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.patch; + +import java.time.LocalDateTime; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.*; +import nl.tudelft.labracore.api.dto.AssignmentPatchDTO; +import nl.tudelft.labracore.api.dto.Description; +import nl.tudelft.librador.dto.create.Create; + +import org.springframework.format.annotation.DateTimeFormat; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class QueueAssignmentPatchDTO extends Create<AssignmentPatchDTO> { + private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm"; + + @NotNull + private Long assignment; + + @NotBlank + private String name; + @NotNull + @Builder.Default + private String description = ""; + + @DateTimeFormat(pattern = TIME_FORMAT) + private LocalDateTime deadline; + + @Override + public Class<AssignmentPatchDTO> clazz() { + return AssignmentPatchDTO.class; + } + + @Override + protected void postApply(AssignmentPatchDTO data) { + data.description(new Description().text(description)); + } +} diff --git a/src/main/java/nl/tudelft/queue/dto/patch/QueueEditionPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueEditionPatchDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..b16291cc3dc78d5f1a5dc08285a47ff6c7c51803 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueEditionPatchDTO.java @@ -0,0 +1,72 @@ +/* + * 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.patch; + +import java.time.LocalDateTime; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.*; +import nl.tudelft.labracore.api.dto.CohortIdDTO; +import nl.tudelft.labracore.api.dto.CourseIdDTO; +import nl.tudelft.labracore.api.dto.EditionPatchDTO; +import nl.tudelft.librador.dto.create.Create; + +import org.springframework.format.annotation.DateTimeFormat; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class QueueEditionPatchDTO extends Create<EditionPatchDTO> { + private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm"; + + @NotNull + private Long edition; + + @NotBlank + private String name; + + @NotNull + private Long course; + + @NotNull + private Long cohort; + + @NotNull + @DateTimeFormat(pattern = TIME_FORMAT) + private LocalDateTime startDate; + + @NotNull + @DateTimeFormat(pattern = TIME_FORMAT) + private LocalDateTime endDate; + + @Override + public Class<EditionPatchDTO> clazz() { + return EditionPatchDTO.class; + } + + @Override + protected void postApply(EditionPatchDTO data) { + super.postApply(data); + data.course(new CourseIdDTO().id(course)); + data.cohort(new CohortIdDTO().id(cohort)); + } +} diff --git a/src/main/java/nl/tudelft/queue/cache/CohortCacheManager.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueModulePatchDTO.java similarity index 54% rename from src/main/java/nl/tudelft/queue/cache/CohortCacheManager.java rename to src/main/java/nl/tudelft/queue/dto/patch/QueueModulePatchDTO.java index cde48d7f70da921b38b39c42eb3f372892b5a02b..0884a93a8b71e5637d484151d9f1f553455d868a 100644 --- a/src/main/java/nl/tudelft/queue/cache/CohortCacheManager.java +++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueModulePatchDTO.java @@ -15,30 +15,30 @@ * 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.cache; +package nl.tudelft.queue.dto.patch; -import java.util.List; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; -import nl.tudelft.labracore.api.CohortControllerApi; -import nl.tudelft.labracore.api.dto.CohortDetailsDTO; +import lombok.*; +import nl.tudelft.labracore.api.dto.ModulePatchDTO; +import nl.tudelft.librador.dto.create.Create; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class QueueModulePatchDTO extends Create<ModulePatchDTO> { + @NotNull + private Long module; -@Component -@RequestScope -public class CohortCacheManager extends CoreCacheManager<Long, CohortDetailsDTO> { - @Autowired - private CohortControllerApi cApi; + @NotBlank + private String name; @Override - protected List<CohortDetailsDTO> fetch(List<Long> ids) { - return cApi.getAllCohortsById(ids).collectList().block(); + public Class<ModulePatchDTO> clazz() { + return ModulePatchDTO.class; } - @Override - protected Long getId(CohortDetailsDTO dto) { - return dto.getId(); - } } diff --git a/src/main/java/nl/tudelft/queue/cache/EditionCacheManager.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueRolePatchDTO.java similarity index 54% rename from src/main/java/nl/tudelft/queue/cache/EditionCacheManager.java rename to src/main/java/nl/tudelft/queue/dto/patch/QueueRolePatchDTO.java index b1ef979159d7339a4b8bcfedef4207692bc3fe84..b71daa5a8de1b9af2b2f4925992222e6dbab650e 100644 --- a/src/main/java/nl/tudelft/queue/cache/EditionCacheManager.java +++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueRolePatchDTO.java @@ -15,30 +15,31 @@ * 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.cache; +package nl.tudelft.queue.dto.patch; -import java.util.List; +import static nl.tudelft.labracore.api.dto.RolePatchDTO.TypeEnum.*; -import nl.tudelft.labracore.api.EditionControllerApi; -import nl.tudelft.labracore.api.dto.EditionDetailsDTO; +import java.util.Set; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.context.annotation.RequestScope; +import lombok.*; +import nl.tudelft.labracore.api.dto.RolePatchDTO; +import nl.tudelft.librador.dto.Validated; -@Component -@RequestScope -public class EditionCacheManager extends CoreCacheManager<Long, EditionDetailsDTO> { - @Autowired - private EditionControllerApi api; +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class QueueRolePatchDTO extends Validated { + private Long edition; + private Long person; + private RolePatchDTO.TypeEnum role; @Override - protected List<EditionDetailsDTO> fetch(List<Long> ids) { - return api.getEditionsById(ids).collectList().block(); + public void validate() { + if (Set.of(ADMIN, TEACHER_RO, BLOCKED).contains(role)) { + errors.rejectValue("type", "The 'type' field for a role cannot be ADMIN, BLOCKED or TEACHER_RO"); + } } - @Override - protected Long getId(EditionDetailsDTO dto) { - return dto.getId(); - } } diff --git a/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java index 631d5945cbec87bf6f7b240b8d7f895082dc5c39..dc0b24076728e78cdc80c9a5a70670088eefe539 100644 --- a/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java @@ -37,6 +37,9 @@ import nl.tudelft.queue.model.embeddables.Slot; @AllArgsConstructor @EqualsAndHashCode(callSuper = false) public abstract class QueueSessionPatchDTO<D extends QueueSession<?>> extends Patch<D> { + + private Long id; + private String name; private Slot slot; diff --git a/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java index 25294984fcfc48b73e22c4faf92e534074aad135..28f2646ed508155cdc0ee2a83c214753a57dc38e 100644 --- a/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java @@ -20,9 +20,9 @@ package nl.tudelft.queue.dto.patch; import java.util.Objects; import lombok.*; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; import nl.tudelft.librador.SpringContext; import nl.tudelft.librador.dto.patch.Patch; -import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.model.LabRequest; @Data diff --git a/src/main/java/nl/tudelft/queue/dto/patch/SlottedLabConfigPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/SlottedLabConfigPatchDTO.java index 07716b5f9e08b015ec71e5ecf16da0397b4e8c6d..207db1f57ec29e64d43ba5a7b0a2bbac25fccce3 100644 --- a/src/main/java/nl/tudelft/queue/dto/patch/SlottedLabConfigPatchDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/patch/SlottedLabConfigPatchDTO.java @@ -31,7 +31,7 @@ import org.springframework.format.annotation.DateTimeFormat; @AllArgsConstructor @EqualsAndHashCode(callSuper = false) public class SlottedLabConfigPatchDTO extends Patch<SlottedLabConfig> { - private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm"; + private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm"; @DateTimeFormat(pattern = TIME_FORMAT) private LocalDateTime selectionOpensAt; diff --git a/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java index 86ef532e4ccc81cfb7ba17eec0c75d60af72fb23..dd2c89878f3fb13f59ba62847ed5e3ed852a30ba 100644 --- a/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java @@ -29,6 +29,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import nl.tudelft.librador.dto.Validated; +import nl.tudelft.queue.model.LabRequest; +import nl.tudelft.queue.model.Request; import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.enums.RequestType; @@ -70,8 +72,25 @@ public class RequestTableFilterDTO extends Validated implements Serializable { * @return A stream of Sets. */ private Stream<Set<?>> getAllFiltersAsStream() { - return Stream.of(labs, assigned, rooms, assigned, assignments, requestStatuses, - requestTypes); + return Stream.of(labs, assignments, rooms, assigned, requestStatuses, requestTypes); + } + + /** + * Checks whether a request matches this filter. + * + * @param request The request to check + * @return True iff the request matches the filter + */ + public boolean apply(Request<?> request) { + return (labs.isEmpty() || labs.contains(request.getSession().getId())) && + (assigned.isEmpty() || assigned.contains(request.getEventInfo().getAssignedTo())) && + (rooms.isEmpty() || rooms.contains(request.getRoom())) && + (assignments.isEmpty() + || request instanceof LabRequest lr && assignments.contains(lr.getAssignment())) + && + (requestStatuses.isEmpty() || requestStatuses.contains(request.getEventInfo().getStatus())) && + (requestTypes.isEmpty() + || request instanceof LabRequest lr && requestTypes.contains(lr.getRequestType())); } @Override diff --git a/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterUpdateDTO.java b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterUpdateDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..64fb81d87c016291181b032983f3f0b402b159f7 --- /dev/null +++ b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterUpdateDTO.java @@ -0,0 +1,69 @@ +/* + * 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.util; + +import java.io.Serializable; +import java.util.Set; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import nl.tudelft.librador.dto.Validated; +import nl.tudelft.queue.model.enums.RequestStatus; +import nl.tudelft.queue.model.enums.RequestType; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class RequestTableFilterUpdateDTO extends Validated implements Serializable { + + private Set<Long> labs; + private Set<Long> assignments; + private Set<Long> rooms; + private Set<Long> assigned; + private Set<RequestStatus> requestStatuses; + private Set<RequestType> requestTypes; + + /** + * Updates an existing filter with this DTO. + * + * @param filter The filter to update + * @return The updated filter + */ + public RequestTableFilterDTO updateFilter(RequestTableFilterDTO filter) { + if (labs != null) + filter.setLabs(labs); + if (assignments != null) + filter.setAssignments(assignments); + if (rooms != null) + filter.setRooms(rooms); + if (assigned != null) + filter.setAssigned(assigned); + if (requestStatuses != null) + filter.setRequestStatuses(requestStatuses); + if (requestTypes != null) + filter.setRequestTypes(requestTypes); + return filter; + } + + @Override + protected void validate() { + } +} diff --git a/src/main/java/nl/tudelft/queue/dto/view/CalendarEntryViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/CalendarEntryViewDTO.java index 5fa5277ca295ae5bad77bd4ec078e2d0b43c343a..8b61ae319b3ead9507365b80e41d4ffc9020477a 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/CalendarEntryViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/CalendarEntryViewDTO.java @@ -24,9 +24,9 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import nl.tudelft.labracore.api.dto.SessionDetailsDTO; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; import nl.tudelft.librador.SpringContext; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.model.enums.QueueSessionType; import nl.tudelft.queue.model.labs.AbstractSlottedLab; import nl.tudelft.queue.model.labs.Lab; diff --git a/src/main/java/nl/tudelft/queue/dto/view/ExamRequestViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/ExamRequestViewDTO.java index 519bf9242b1325bbc981465c8965be749eb2e2e5..e6506a24e3e3d425921710e5b7582f5e577aa1dd 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/ExamRequestViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/ExamRequestViewDTO.java @@ -23,8 +23,8 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.PersonCacheManager; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.model.embeddables.RequestEventInfo; diff --git a/src/main/java/nl/tudelft/queue/dto/view/ExamTimeSlotViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/ExamTimeSlotViewDTO.java index 0c265b3d65b4d1cc5fa9da0525f7dd204e523720..4ca793457b7eca2320ad7ea03740fcf850e9926d 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/ExamTimeSlotViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/ExamTimeSlotViewDTO.java @@ -27,12 +27,14 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.PersonCacheManager; import nl.tudelft.queue.model.ClosableTimeSlot; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.model.embeddables.Slot; +import com.fasterxml.jackson.annotation.JsonIgnore; + @Data @NoArgsConstructor @AllArgsConstructor @@ -48,6 +50,7 @@ public class ExamTimeSlotViewDTO extends View<ClosableTimeSlot> { private Boolean active; + @JsonIgnore private List<ExamRequestViewDTO> examRequests; @Override diff --git a/src/main/java/nl/tudelft/queue/dto/view/FeedbackViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/FeedbackViewDTO.java index 820b8cf1f57f3d61a6f3661d5525ab7e5be89a6f..d4ec22042beb4a28f86517a6776ae8b1aaf02f06 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/FeedbackViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/FeedbackViewDTO.java @@ -29,9 +29,9 @@ import lombok.NoArgsConstructor; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; import nl.tudelft.labracore.api.dto.RolePersonLayer1DTO; import nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.cache.StudentGroupCacheManager; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.SessionCacheManager; -import nl.tudelft.queue.cache.StudentGroupCacheManager; import nl.tudelft.queue.model.Feedback; import nl.tudelft.queue.model.LabRequest; diff --git a/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java index e7799ea679224a2de128a21f86d69bf3e439156a..d1cacc992e68aeae50e08ab091dee68f6d30bdf4 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java @@ -34,4 +34,6 @@ public abstract class LabViewDTO<D extends Lab> extends QueueSessionViewDTO<D> { private Set<AllowedRequest> allowedRequests; private Boolean enableExperimental; + + private Integer eolGracePeriod; } diff --git a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java index 16cad7b0f5aab595ee730635a90cb6af3fd6d672..8e1d8dbd30b588a32e2fb80cc2e36834a992f6ee 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java @@ -26,8 +26,8 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.misc.QueueSessionStatus; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.TimeSlot; diff --git a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionViewDTO.java index efef4fad47d4c9e6928635b36b6a3d33a5749367..5a43fa2a46ba681ae3fbd4e0928e377161b5ea92 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionViewDTO.java @@ -31,9 +31,9 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import nl.tudelft.labracore.api.dto.ModuleDetailsDTO; import nl.tudelft.labracore.api.dto.SessionDetailsDTO; +import nl.tudelft.labracore.lib.cache.ModuleCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.ModuleCacheManager; -import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.misc.QueueSessionStatus; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.Request; @@ -41,9 +41,7 @@ import nl.tudelft.queue.model.embeddables.LabRequestConstraints; import nl.tudelft.queue.model.enums.QueueSessionType; import nl.tudelft.queue.service.LabService; -import org.commonmark.node.Node; -import org.commonmark.parser.Parser; -import org.commonmark.renderer.html.HtmlRenderer; +import com.fasterxml.jackson.annotation.JsonIgnore; @Data @NoArgsConstructor @@ -63,6 +61,7 @@ public abstract class QueueSessionViewDTO<D extends QueueSession<?>> extends Vie private Set<ModuleDetailsDTO> modules; + @JsonIgnore private List<Request<?>> requests = new ArrayList<>(); private QueueSessionStatus status; @@ -76,12 +75,12 @@ public abstract class QueueSessionViewDTO<D extends QueueSession<?>> extends Vie getBean(ModuleCacheManager.class).get(data.getModules().stream())); status = getBean(LabService.class).getLabStatus(data); - Parser parser = Parser.builder() - .build(); - Node document = parser.parse(data.getExtraInfo() == null ? "" : data.getExtraInfo()); - HtmlRenderer renderer = HtmlRenderer.builder().sanitizeUrls(true).escapeHtml(true) - .build(); - extraInfo = renderer.render(document); + // Parser parser = Parser.builder() + // .build(); + // Node document = parser.parse(data.getExtraInfo() == null ? "" : data.getExtraInfo()); + // HtmlRenderer renderer = HtmlRenderer.builder().sanitizeUrls(true).escapeHtml(true) + // .build(); TODO + extraInfo = data.getExtraInfo() == null ? "" : data.getExtraInfo(); //renderer.render(document); } /** diff --git a/src/main/java/nl/tudelft/queue/dto/view/RequestEventInfoViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/RequestEventInfoViewDTO.java index 26bf22ef2cf184adfe8fd058d671aff50d644984..95cf1362bc0485f0d0dfeb067f27b93fa03772fc 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/RequestEventInfoViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/RequestEventInfoViewDTO.java @@ -25,9 +25,9 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.librador.SpringContext; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.PersonCacheManager; import nl.tudelft.queue.model.RequestEvent; import nl.tudelft.queue.model.embeddables.RequestEventInfo; import nl.tudelft.queue.model.enums.RequestStatus; diff --git a/src/main/java/nl/tudelft/queue/dto/view/RequestViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/RequestViewDTO.java index 7a9cfa5e03592f35bc49db0c9a4df96ae432d22b..799af77ac9407df68fa51973ecf6eb7e46e6e0bf 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/RequestViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/RequestViewDTO.java @@ -31,8 +31,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.labracore.lib.cache.*; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.*; import nl.tudelft.queue.csv.CsvAble; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.Request; diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestApprovedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestApprovedEventViewDTO.java index 41dd0c9eafaa835e5a8a955507aa2beb68bd983d..61a4ae56befec140beda44848b7713bcf32b6306 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestApprovedEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestApprovedEventViewDTO.java @@ -29,6 +29,6 @@ public class RequestApprovedEventViewDTO extends RequestHandledEventViewDTO<Requ @Override public String getIconClass() { - return "request-approved"; + return "positive"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestCreatedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestCreatedEventViewDTO.java index 8362d73d07984473be5d63412ad29baeebe60009..2580a71526201f32a4fb3e483ee60c083fb95153 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestCreatedEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestCreatedEventViewDTO.java @@ -30,6 +30,6 @@ public class RequestCreatedEventViewDTO extends RequestEventViewDTO<RequestCreat @Override public String getIconClass() { - return "request-created"; + return "neutral"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToAnyEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToAnyEventViewDTO.java index 4e3925c7640805b1a1a4421f1f1cfb4b10941ac5..82470d9b543cdcd49cb3d0b2f605aed196252be9 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToAnyEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToAnyEventViewDTO.java @@ -43,6 +43,6 @@ public class RequestForwardedToAnyEventViewDTO extends RequestEventViewDTO<Reque @Override public String getIconClass() { - return "request-forwarded"; + return "neutral"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToPersonEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToPersonEventViewDTO.java index 09201cbd923ba079af05d997c7ff624e153d306f..3793b01351119bd30cacf17bd853224fa14ccfd3 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToPersonEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToPersonEventViewDTO.java @@ -24,7 +24,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; -import nl.tudelft.queue.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.queue.dto.view.RequestEventViewDTO; import nl.tudelft.queue.model.events.RequestForwardedToPersonEvent; @@ -53,6 +53,6 @@ public class RequestForwardedToPersonEventViewDTO extends RequestEventViewDTO<Re @Override public String getIconClass() { - return "request-forwarded"; + return "neutral"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestHandledEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestHandledEventViewDTO.java index 22c6aa8200526e83b6136cccf42c663d3d4bd1c0..8654afac0ebc1b2228a93fafab380485c3265722 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestHandledEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestHandledEventViewDTO.java @@ -24,7 +24,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; -import nl.tudelft.queue.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.queue.dto.view.RequestEventViewDTO; import nl.tudelft.queue.model.events.RequestHandledEvent; diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotPickedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotPickedEventViewDTO.java index e9ef3b90fc0bbc305c91d9d5b2b5dbcf3f10f3de..6ed6ca9c2a33b28d898faca14e0cd7ef2657925a 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotPickedEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotPickedEventViewDTO.java @@ -28,6 +28,6 @@ public class RequestNotPickedEventViewDTO extends RequestEventViewDTO<RequestNot @Override public String getIconClass() { - return "request-not-picked"; + return "greyed-out"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotSelectedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotSelectedEventViewDTO.java index 3c371829c6b447b51220e29897a6b9f37fc08bc3..784d224cbb77048c226849e604fbeb892de723c3 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotSelectedEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotSelectedEventViewDTO.java @@ -28,6 +28,6 @@ public class RequestNotSelectedEventViewDTO extends RequestEventViewDTO<RequestN @Override public String getIconClass() { - return "request-not-selected"; + return "negative"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestPickedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestPickedEventViewDTO.java index 67db9917b086ef851a923104ef34e7f4ec14c241..809834a5f6d99d3d3d6d9e1482524f6206f8381f 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestPickedEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestPickedEventViewDTO.java @@ -24,7 +24,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; -import nl.tudelft.queue.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.queue.dto.view.RequestEventViewDTO; import nl.tudelft.queue.model.events.RequestPickedEvent; @@ -47,6 +47,6 @@ public class RequestPickedEventViewDTO extends RequestEventViewDTO<RequestPicked @Override public String getIconClass() { - return "request-taken"; + return "neutral"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestRejectedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestRejectedEventViewDTO.java index 22411d2dd27ae8b5660ecf3854f7e64f47ae013d..bd0a709bdbe8145deade8b1325f5ad4f9d410ad6 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestRejectedEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestRejectedEventViewDTO.java @@ -29,6 +29,6 @@ public class RequestRejectedEventViewDTO extends RequestHandledEventViewDTO<Requ @Override public String getIconClass() { - return "request-rejected"; + return "negative"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestRevokedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestRevokedEventViewDTO.java index 25ec29c058b58b9f314ff82b131acf1b55a9be82..5cfc4576257642b6f209cd4755a66a598d5efc6e 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestRevokedEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestRevokedEventViewDTO.java @@ -30,6 +30,6 @@ public class RequestRevokedEventViewDTO extends RequestEventViewDTO<RequestRevok @Override public String getIconClass() { - return "request-revoked"; + return "greyed-out"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestSelectedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestSelectedEventViewDTO.java index 7b50ae0d332bad13b62667203d5dca488c2ac609..8eab9e6ae447356bdda7793cb1d214eea7149bfb 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestSelectedEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestSelectedEventViewDTO.java @@ -28,6 +28,6 @@ public class RequestSelectedEventViewDTO extends RequestEventViewDTO<RequestSele @Override public String getIconClass() { - return "request-selected"; + return "positive"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestTakenEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestTakenEventViewDTO.java index 2054839fef59804d8cdd412fc1eaeb027b356279..a371c4a84d5ba53e6b28394c9bb7ed592ccb8830 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestTakenEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestTakenEventViewDTO.java @@ -24,7 +24,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; -import nl.tudelft.queue.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.queue.dto.view.RequestEventViewDTO; import nl.tudelft.queue.model.events.RequestTakenEvent; @@ -47,6 +47,6 @@ public class RequestTakenEventViewDTO extends RequestEventViewDTO<RequestTakenEv @Override public String getIconClass() { - return "request-taken"; + return "neutral"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java index cae29aee2f293b9fa4f7164c5c8f3fa44a6123bc..feb2728fd506ccbbc4a50cf50771dc1c2eaacd87 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java @@ -53,6 +53,6 @@ public class StudentNotFoundEventViewDTO extends RequestEventViewDTO<StudentNotF @Override public String getIconClass() { - return "request-not-found"; + return "neutral"; } } diff --git a/src/main/java/nl/tudelft/queue/dto/view/labs/ExamLabViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/labs/ExamLabViewDTO.java index 04e867692357053f791cdc9fe04670b942022cbc..b97805e1fa46cb86a49097e3bad4367a146f6da7 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/labs/ExamLabViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/labs/ExamLabViewDTO.java @@ -18,7 +18,6 @@ package nl.tudelft.queue.dto.view.labs; import java.util.List; -import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.Data; @@ -30,8 +29,6 @@ import nl.tudelft.queue.model.embeddables.ExamLabConfig; import nl.tudelft.queue.model.labs.ExamLab; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; @Data @NoArgsConstructor @@ -45,7 +42,7 @@ public class ExamLabViewDTO extends AbstractSlottedLabViewDTO<ExamLab> { private int page; private int pages; - private List<Page<ExamTimeSlotViewDTO>> timeSlotPages; + private List<ExamTimeSlotViewDTO> examTimeSlots; private int totalHandled; private int totalNeeded; @@ -58,13 +55,7 @@ public class ExamLabViewDTO extends AbstractSlottedLabViewDTO<ExamLab> { page = currentPage(); pages = data.getTimeSlots().size() / SLOTS_PER_PAGE + 1; - List<ExamTimeSlotViewDTO> timeSlots = View.convert(data.getTimeSlots(), ExamTimeSlotViewDTO.class); - timeSlotPages = timeSlots.stream() - .collect(Collectors.groupingBy(t -> timeSlots.indexOf(t) / 4)) - .entrySet().stream() - .map(e -> new PageImpl<>(e.getValue(), PageRequest.of(e.getKey(), pages), - data.getTimeSlots().size())) - .collect(Collectors.toList()); + examTimeSlots = View.convert(data.getTimeSlots(), ExamTimeSlotViewDTO.class); totalHandled = data.getHandled().size(); totalNeeded = (int) Math .ceil(data.getRequests().size() * ((double) examLabConfig.getPercentage() / 100.0)); diff --git a/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java index 67072fb3a5e71543b59b758ed1dbdc8f73aaab2b..769eca6b4655da57f4a0c6dbc5d074d16d976789 100644 --- a/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java +++ b/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java @@ -27,9 +27,9 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import nl.tudelft.labracore.api.dto.AssignmentDetailsDTO; -import nl.tudelft.queue.cache.AssignmentCacheManager; -import nl.tudelft.queue.cache.EditionCacheManager; -import nl.tudelft.queue.cache.ModuleCacheManager; +import nl.tudelft.labracore.lib.cache.AssignmentCacheManager; +import nl.tudelft.labracore.lib.cache.EditionCacheManager; +import nl.tudelft.labracore.lib.cache.ModuleCacheManager; import nl.tudelft.queue.dto.view.RequestViewDTO; import nl.tudelft.queue.model.Feedback; import nl.tudelft.queue.model.LabRequest; diff --git a/src/main/java/nl/tudelft/queue/model/TimeSlot.java b/src/main/java/nl/tudelft/queue/model/TimeSlot.java index 18b412308db8f6d581df869126698c6ae43e021d..e4c154d58d271d6b08fd257a953f154edc07829a 100644 --- a/src/main/java/nl/tudelft/queue/model/TimeSlot.java +++ b/src/main/java/nl/tudelft/queue/model/TimeSlot.java @@ -33,6 +33,8 @@ import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.labs.AbstractSlottedLab; import nl.tudelft.queue.model.labs.Lab; +import com.fasterxml.jackson.annotation.JsonIgnore; + /** * Entity class representing a time-slot created for a certain lab. Labs create time-slots when the lab is set * to be a slotted lab. @@ -62,6 +64,7 @@ public class TimeSlot { */ @NotNull @ManyToOne + @JsonIgnore @JoinColumn(name = "lab_id", updatable = false) private Lab lab; @@ -88,6 +91,7 @@ public class TimeSlot { * The requests that currently claim this slot. */ @NotNull + @JsonIgnore @ToString.Exclude @EqualsAndHashCode.Exclude @OneToMany(mappedBy = "timeSlot") diff --git a/src/main/java/nl/tudelft/queue/model/constraints/ClusterConstraint.java b/src/main/java/nl/tudelft/queue/model/constraints/ClusterConstraint.java index 66e9b63acddec2c5d230aa9747bd14be4b6664f0..3888c3f38686c981bdbfc243995cfe2f8537fd8a 100644 --- a/src/main/java/nl/tudelft/queue/model/constraints/ClusterConstraint.java +++ b/src/main/java/nl/tudelft/queue/model/constraints/ClusterConstraint.java @@ -34,12 +34,14 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import nl.tudelft.labracore.api.dto.ClusterDetailsDTO; +import nl.tudelft.labracore.lib.cache.ClusterCacheManager; import nl.tudelft.librador.SpringContext; -import nl.tudelft.queue.cache.ClusterCacheManager; import nl.tudelft.queue.dto.create.constraints.ClusterConstraintCreateDTO; import nl.tudelft.queue.model.LabRequestConstraint; import nl.tudelft.queue.model.QueueSession; +import com.fasterxml.jackson.annotation.JsonIgnore; + @Data @Entity @SuperBuilder @@ -59,6 +61,7 @@ public class ClusterConstraint extends LabRequestConstraint { */ @NotNull @OneToOne + @JsonIgnore private QueueSession<?> session; @Override diff --git a/src/main/java/nl/tudelft/queue/model/constraints/ModuleDivisionConstraint.java b/src/main/java/nl/tudelft/queue/model/constraints/ModuleDivisionConstraint.java index b21b5ac18e32071216a4c5434d2b00e9c8af1c04..cce0f7bbda32a1494c02b60a8bbf246d3a833313 100644 --- a/src/main/java/nl/tudelft/queue/model/constraints/ModuleDivisionConstraint.java +++ b/src/main/java/nl/tudelft/queue/model/constraints/ModuleDivisionConstraint.java @@ -31,15 +31,16 @@ import javax.validation.constraints.NotNull; import lombok.*; import lombok.experimental.SuperBuilder; import nl.tudelft.labracore.api.ModuleDivisionControllerApi; -import nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.StudentGroupCacheManager; import nl.tudelft.librador.SpringContext; -import nl.tudelft.queue.cache.PersonCacheManager; -import nl.tudelft.queue.cache.StudentGroupCacheManager; import nl.tudelft.queue.dto.create.constraints.ModuleDivisionConstraintCreateDTO; import nl.tudelft.queue.model.LabRequestConstraint; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.Request; +import com.fasterxml.jackson.annotation.JsonIgnore; + @Data @Entity @SuperBuilder @@ -59,14 +60,15 @@ public class ModuleDivisionConstraint extends LabRequestConstraint { */ @NotNull @OneToOne + @JsonIgnore private QueueSession<?> session; @Override public boolean canCreateRequest(Long personId) { // Go through every one of the student groups the person is in and check whether they are in one that is part // of an allowed module division. - Stream<Long> studentGroups = SpringContext.getBean(StudentGroupCacheManager.class) - .getByPerson(personId).stream().map(StudentGroupDetailsDTO::getId); + Stream<Long> studentGroups = null; // TODO SpringContext.getBean(StudentGroupCacheManager.class) + // .getByPerson(personId).stream().map(StudentGroupDetailsDTO::getId); return studentInDivision(personId) || studentGroupInDivision(studentGroups); } diff --git a/src/main/java/nl/tudelft/queue/model/embeddables/Slot.java b/src/main/java/nl/tudelft/queue/model/embeddables/Slot.java index 6f2fd10a2816711704b278b38e593995d7dcf7f0..7db85f7b1a6137506889c8206d6ce9a253893906 100644 --- a/src/main/java/nl/tudelft/queue/model/embeddables/Slot.java +++ b/src/main/java/nl/tudelft/queue/model/embeddables/Slot.java @@ -39,7 +39,7 @@ import org.springframework.format.annotation.DateTimeFormat; @NoArgsConstructor @AllArgsConstructor public class Slot { - private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm"; + private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm"; private static final String SIMPLE_SLOT_FORMAT = "HH:mm"; /** diff --git a/src/main/java/nl/tudelft/queue/model/enums/RequestStatus.java b/src/main/java/nl/tudelft/queue/model/enums/RequestStatus.java index 667fe0a7f1f2cd588cd52611749031c43f453a77..8462471eda2ac6ec96ea54e48ae23dfa4f5b06c1 100644 --- a/src/main/java/nl/tudelft/queue/model/enums/RequestStatus.java +++ b/src/main/java/nl/tudelft/queue/model/enums/RequestStatus.java @@ -111,11 +111,16 @@ public enum RequestStatus { } /** - * Gets the CSS class that determines the colour that this status will be displayed as. + * Gets the data-type that determines the colour that this status will be displayed as. * - * @return The name of the class that determines the colour for this status. + * @return The name of the data-type that determines the colour for this status. */ public String getColourClass() { - return "bg-" + name().toLowerCase(); + return switch (this) { + case APPROVED, SELECTED -> "positive"; + case REJECTED, NOTSELECTED -> "negative"; + case REVOKED -> "greyed-out"; + default -> "regular"; + }; } } diff --git a/src/main/java/nl/tudelft/queue/model/labs/CapacitySession.java b/src/main/java/nl/tudelft/queue/model/labs/CapacitySession.java index a419aeaaa21cc2d4436aae8dd32f418fdd2d1f4e..f80b3ebc6c1e4f536ee7a8f903dfafa9f429ef07 100644 --- a/src/main/java/nl/tudelft/queue/model/labs/CapacitySession.java +++ b/src/main/java/nl/tudelft/queue/model/labs/CapacitySession.java @@ -33,8 +33,8 @@ import lombok.*; import lombok.experimental.SuperBuilder; import nl.tudelft.labracore.api.dto.RoomDetailsDTO; import nl.tudelft.labracore.api.dto.SessionDetailsDTO; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.SessionCacheManager; import nl.tudelft.queue.dto.create.QueueSessionCreateDTO; import nl.tudelft.queue.dto.create.labs.CapacitySessionCreateDTO; import nl.tudelft.queue.dto.patch.QueueSessionPatchDTO; diff --git a/src/main/java/nl/tudelft/queue/model/labs/Lab.java b/src/main/java/nl/tudelft/queue/model/labs/Lab.java index 5d88cf2213aa1e7ad40bef1dc2c8ecdbe372c9bf..dc49e41f8acd236ab33e075ecca1ca84c0e72131 100644 --- a/src/main/java/nl/tudelft/queue/model/labs/Lab.java +++ b/src/main/java/nl/tudelft/queue/model/labs/Lab.java @@ -35,7 +35,7 @@ import javax.validation.constraints.NotNull; import lombok.*; import lombok.experimental.SuperBuilder; -import nl.tudelft.queue.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.model.QueueSession; import nl.tudelft.queue.model.Request; diff --git a/src/main/java/nl/tudelft/queue/realtime/SubscriptionContainer.java b/src/main/java/nl/tudelft/queue/realtime/SubscriptionContainer.java index e8263e41c93aea1d37a4c17e8787b5907eb3bee5..15c8f77a383b3919c9d61c6f997b29ef4eaa4a87 100644 --- a/src/main/java/nl/tudelft/queue/realtime/SubscriptionContainer.java +++ b/src/main/java/nl/tudelft/queue/realtime/SubscriptionContainer.java @@ -22,7 +22,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import lombok.NoArgsConstructor; -import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.queue.dto.create.SubscriptionCreateDTO; import nl.tudelft.queue.properties.PushProperties; diff --git a/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java b/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java index c0736c9bf9881553c33a3d161a0725368b889f4b..cd0ae241ec9b304b2523c9e3df93fd8a213bff5b 100644 --- a/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java +++ b/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java @@ -19,14 +19,19 @@ package nl.tudelft.queue.realtime.messages; import static nl.tudelft.librador.SpringContext.getBean; +import java.util.Locale; + import lombok.*; +import nl.tudelft.labracore.lib.cache.ModuleCacheManager; +import nl.tudelft.librador.SpringContext; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.ModuleCacheManager; import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.enums.RequestType; +import org.springframework.context.MessageSource; + @Data @Builder @NoArgsConstructor @@ -54,9 +59,11 @@ public class RequestCreatedMessage extends View<LabRequest> implements Message { private String assignmentName; private Long moduleId; private String moduleName; + private String courseName; private Long labId; private RequestStatus status; + private String statusDisplayName; private RequestType requestType; private String requestTypeDisplayName; @@ -93,10 +100,14 @@ public class RequestCreatedMessage extends View<LabRequest> implements Message { assignmentName = view.getAssignment().getName(); moduleId = view.getAssignment().getModule().getId(); moduleName = view.getAssignment().getModule().getName(); + courseName = view.getEdition().getCourse().getName(); labId = view.getQSession().getId(); status = RequestStatus.PENDING; + statusDisplayName = SpringContext.getBean(MessageSource.class).getMessage( + "request.status." + status.name().toLowerCase(), new String[0], Locale.getDefault()); + requestTypeDisplayName = view.getRequestType().displayName(); } } diff --git a/src/main/java/nl/tudelft/queue/realtime/messages/RequestStatusUpdateMessage.java b/src/main/java/nl/tudelft/queue/realtime/messages/RequestStatusUpdateMessage.java index 0e56818f0878827223601ca8932bd8932dec883a..b8e814dbe38e61b8aa8a5ed42ec19e5f122550a1 100644 --- a/src/main/java/nl/tudelft/queue/realtime/messages/RequestStatusUpdateMessage.java +++ b/src/main/java/nl/tudelft/queue/realtime/messages/RequestStatusUpdateMessage.java @@ -17,14 +17,19 @@ */ package nl.tudelft.queue.realtime.messages; +import java.util.Locale; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import nl.tudelft.librador.SpringContext; import nl.tudelft.librador.dto.view.View; import nl.tudelft.queue.model.RequestEvent; import nl.tudelft.queue.model.enums.RequestStatus; +import org.springframework.context.MessageSource; + @Data @NoArgsConstructor @AllArgsConstructor @@ -37,6 +42,11 @@ public abstract class RequestStatusUpdateMessage<T extends RequestEvent> extends public abstract RequestStatus getStatus(); + public String getStatusDisplayName() { + return SpringContext.getBean(MessageSource.class).getMessage( + "request.status." + getStatus().name().toLowerCase(), new String[0], Locale.getDefault()); + } + @Override public void postApply() { id = data.getRequest().getId(); diff --git a/src/main/java/nl/tudelft/queue/realtime/messages/RequestTakenMessage.java b/src/main/java/nl/tudelft/queue/realtime/messages/RequestTakenMessage.java index b269bb099835c8b16b12e04fd9b137f1048b1b94..51a9e315498d9a120291dff76cac901c85e6140c 100644 --- a/src/main/java/nl/tudelft/queue/realtime/messages/RequestTakenMessage.java +++ b/src/main/java/nl/tudelft/queue/realtime/messages/RequestTakenMessage.java @@ -20,7 +20,8 @@ package nl.tudelft.queue.realtime.messages; import static nl.tudelft.librador.SpringContext.getBean; import lombok.*; -import nl.tudelft.queue.cache.PersonCacheManager; +import nl.tudelft.labracore.api.dto.PersonSummaryDTO; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.events.RequestTakenEvent; @@ -32,13 +33,16 @@ import nl.tudelft.queue.model.events.RequestTakenEvent; public class RequestTakenMessage extends RequestStatusUpdateMessage<RequestTakenEvent> { private static final long serialVersionUID = -2710218413754443043L; + private Long takenById; private String takenBy; @Override public void postApply() { super.postApply(); - takenBy = getBean(PersonCacheManager.class).getOrThrow(data.getAssistant()).getDisplayName(); + PersonSummaryDTO assistant = getBean(PersonCacheManager.class).getOrThrow(data.getAssistant()); + takenById = assistant.getId(); + takenBy = assistant.getDisplayName(); } @Override diff --git a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java index 658c90981b4ea0f34eae60521f6ce2acf0853746..4a742a7f160a3665616c422cce89d403c7ee6062 100644 --- a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java +++ b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java @@ -27,7 +27,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import nl.tudelft.labracore.api.dto.AssignmentSummaryDTO; -import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.queue.dto.util.RequestTableFilterDTO; import nl.tudelft.queue.model.*; import nl.tudelft.queue.model.enums.QueueSessionType; @@ -35,14 +35,13 @@ import nl.tudelft.queue.model.enums.RequestStatus; import nl.tudelft.queue.model.labs.AbstractSlottedLab; import nl.tudelft.queue.model.labs.Lab; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; +import org.springframework.data.domain.*; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.lang.NonNull; +import com.google.common.collect.ImmutableList; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; @@ -50,6 +49,10 @@ public interface LabRequestRepository extends JpaRepository<LabRequest, Long>, QuerydslPredicateExecutor<LabRequest> { QLabRequest qlr = QLabRequest.labRequest; + default LabRequest findByIdOrThrow(Long id) { + return findById(id).orElseThrow(() -> new ResourceNotFoundException("Lab request not found")); + } + @NonNull @Override List<LabRequest> findAll(@NonNull Predicate predicate); @@ -328,6 +331,28 @@ public interface LabRequestRepository .and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10))))))))); } + /** + * Counts all requests that pass the given filter for a person. This filter is a DTO transferred from the + * page 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 person The person + * @param filter The filter to apply to the boolean expression. + * @return The amount of requests. + */ + default long countByFilterForPerson(List<Lab> labs, Person person, RequestTableFilterDTO filter) { + return count(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)))))) + .and(isStatusOrForwardedToAny(RequestStatus.PENDING, person) + .or(isStatusOrForwarded(RequestStatus.PENDING, + person)))))); + } + /** * Finds all requests that pass the given filter. This filter is a DTO transferred from the page * requesting to see these requests. The filter contains all labs, rooms, assignments, etc. that need to @@ -340,13 +365,45 @@ public interface LabRequestRepository */ default Page<LabRequest> findAllByFilter(List<Lab> labs, RequestTableFilterDTO filter, Pageable pageable) { - return findAll(qlr.in(select(qlr).from(qlr) + BooleanExpression predicate = 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)))))))); + List<LabRequest> requests = ImmutableList.copyOf(findAll(predicate, qlr.createdAt.asc())); + var page = requests.subList(pageable.getPageNumber() * pageable.getPageSize(), + Math.min(requests.size(), (pageable.getPageNumber() + 1) * pageable.getPageSize())); + return new PageImpl<>(page, pageable, count(predicate)); + } + + /** + * Finds all requests that pass the given filter for a person. This filter is a DTO transferred from the + * page 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. Only requests + * the person is able to handle are returned. + * + * @param labs The labs that the should also be kept in the filter. + * @param filter The filter to apply to the boolean expression. + * @param person The person + * @param pageable The pageable object representing the page. + * @return The filtered list of requests. + */ + default Page<LabRequest> findAllByFilterForPerson(List<Lab> labs, RequestTableFilterDTO filter, + Person person, Pageable pageable) { + BooleanExpression predicate = 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)))))) + .and(isStatusOrForwarded(RequestStatus.PENDING, person) + .or(isStatusOrForwardedToAny(RequestStatus.PENDING, + person))))); + List<LabRequest> requests = ImmutableList.copyOf(findAll(predicate, qlr.createdAt.asc())); + var page = requests.subList(pageable.getPageNumber() * pageable.getPageSize(), + Math.min(requests.size(), (pageable.getPageNumber() + 1) * pageable.getPageSize())); + return new PageImpl<>(page, pageable, count(predicate)); } /** diff --git a/src/main/java/nl/tudelft/queue/repository/QueueSessionRepository.java b/src/main/java/nl/tudelft/queue/repository/QueueSessionRepository.java index de06984c4e0f2b70aa06a85b769c80cf255a5cbb..270b8ceefada757a06aa74cc10383ae0000cffe7 100644 --- a/src/main/java/nl/tudelft/queue/repository/QueueSessionRepository.java +++ b/src/main/java/nl/tudelft/queue/repository/QueueSessionRepository.java @@ -22,6 +22,8 @@ import java.util.List; import nl.tudelft.queue.model.QQueueSession; import nl.tudelft.queue.model.QueueSession; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; + public interface QueueSessionRepository extends QueueRepository<QueueSession<?>, Long> { QQueueSession qs = QQueueSession.queueSession; @@ -33,4 +35,7 @@ public interface QueueSessionRepository extends QueueRepository<QueueSession<?>, return findAll(qs.session.in(sessions)); } + default QueueSession<?> findByIdOrThrow(Long id) { + return findById(id).orElseThrow(() -> new ResourceNotFoundException("Queue Session not found")); + } } diff --git a/src/main/java/nl/tudelft/queue/repository/RequestRepository.java b/src/main/java/nl/tudelft/queue/repository/RequestRepository.java index 0afcabb9bd783df1a8ebb7d93fac08679da6e510..f09c8679e0cbc0b5375285331a2fd1458006ab44 100644 --- a/src/main/java/nl/tudelft/queue/repository/RequestRepository.java +++ b/src/main/java/nl/tudelft/queue/repository/RequestRepository.java @@ -19,5 +19,12 @@ package nl.tudelft.queue.repository; import nl.tudelft.queue.model.Request; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; + public interface RequestRepository extends QueueRepository<Request<?>, Long> { + + default Request<?> findByIdOrThrow(Long id) { + return findById(id).orElseThrow(() -> new ResourceNotFoundException("Request not found")); + } + } diff --git a/src/main/java/nl/tudelft/queue/security/DevUserDetailsProvider.java b/src/main/java/nl/tudelft/queue/security/DevUserDetailsProvider.java index b3b0c90d7580c2dec9ecafa4bcb5b822e33835c8..4d21e0bd69086e83448f084b95da37615458f41d 100644 --- a/src/main/java/nl/tudelft/queue/security/DevUserDetailsProvider.java +++ b/src/main/java/nl/tudelft/queue/security/DevUserDetailsProvider.java @@ -20,8 +20,8 @@ package nl.tudelft.queue.security; import javax.transaction.Transactional; import nl.tudelft.labracore.api.PersonControllerApi; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.memory.InMemoryUserProvider; -import nl.tudelft.labracore.lib.security.user.Person; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/nl/tudelft/queue/security/QueueUserHandler.java b/src/main/java/nl/tudelft/queue/security/QueueUserHandler.java index 224d16275cfe788bfa8b4edded982764b6ce095a..c04e252fa9c01f7587de5428c8d3be914f2af71b 100644 --- a/src/main/java/nl/tudelft/queue/security/QueueUserHandler.java +++ b/src/main/java/nl/tudelft/queue/security/QueueUserHandler.java @@ -17,8 +17,8 @@ */ package nl.tudelft.queue.security; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.LabradorUserHandler; -import nl.tudelft.labracore.lib.security.user.Person; import org.springframework.stereotype.Service; diff --git a/src/main/java/nl/tudelft/queue/service/CapacitySessionService.java b/src/main/java/nl/tudelft/queue/service/CapacitySessionService.java index b7817b290e1e5f5d05ff36d8c93bcb3561138b0c..fc63b2e97697ed745c737ffeef619f04d70f0434 100644 --- a/src/main/java/nl/tudelft/queue/service/CapacitySessionService.java +++ b/src/main/java/nl/tudelft/queue/service/CapacitySessionService.java @@ -29,9 +29,9 @@ import nl.tudelft.labracore.api.StudentGroupControllerApi; import nl.tudelft.labracore.api.dto.RolePersonLayer1DTO; import nl.tudelft.labracore.api.dto.RoomDetailsDTO; import nl.tudelft.labracore.api.dto.SessionDetailsDTO; -import nl.tudelft.queue.cache.PersonCacheManager; -import nl.tudelft.queue.cache.SessionCacheManager; -import nl.tudelft.queue.cache.StudentGroupCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.cache.StudentGroupCacheManager; import nl.tudelft.queue.model.Request; import nl.tudelft.queue.model.SelectionRequest; import nl.tudelft.queue.model.embeddables.RequestEventInfo; @@ -41,7 +41,6 @@ import nl.tudelft.queue.repository.CapacitySessionRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service @@ -270,8 +269,8 @@ public class CapacitySessionService { * @param labs The labs that selections will be run over. */ private void initializeCaches(List<CapacitySession> labs) { - sCache = new SessionCacheManager(sApi); - sgCache = new StudentGroupCacheManager(sgApi, pCache); + // sCache = new SessionCacheManager(sApi); TODO + // sgCache = new StudentGroupCacheManager(sgApi, pCache); sCache.get(labs.stream().map(CapacitySession::getSession)); sgCache.get(labs.stream().flatMap(lab -> lab.getRequests().stream()) @@ -335,7 +334,7 @@ public class CapacitySessionService { * that are available in the lab. */ @Transactional - @Scheduled(cron = "3 * * * * ?") + // @Scheduled(cron = "3 * * * * ?") TODO public void selectStudentsInCapacityLab() { // Find the labs that selection should be done for. List<CapacitySession> labs = csr.findAllThatShouldPickStudents(); diff --git a/src/main/java/nl/tudelft/queue/service/EditionService.java b/src/main/java/nl/tudelft/queue/service/EditionService.java index 3e00f24752d6179158fd4e53116e2a660e85a6f9..f95545dd7121a5d12b08919db4e95d26ca25b3f3 100644 --- a/src/main/java/nl/tudelft/queue/service/EditionService.java +++ b/src/main/java/nl/tudelft/queue/service/EditionService.java @@ -20,6 +20,7 @@ package nl.tudelft.queue.service; import static java.time.LocalDateTime.now; import static java.time.temporal.ChronoUnit.MINUTES; import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; import java.io.IOException; import java.util.*; @@ -34,7 +35,7 @@ import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.PersonControllerApi; import nl.tudelft.labracore.api.RoleControllerApi; import nl.tudelft.labracore.api.dto.*; -import nl.tudelft.queue.cache.AssignmentCacheManager; +import nl.tudelft.labracore.lib.cache.AssignmentCacheManager; import nl.tudelft.queue.csv.CsvHelper; import nl.tudelft.queue.csv.EmptyCsvException; import nl.tudelft.queue.csv.InvalidCsvException; @@ -86,28 +87,29 @@ public class EditionService { private AssignmentCacheManager acm; /** - * Filters a list of people on their username, display name and student number. If any of the + * Filters a list of roles on their username, display name and student number. If any of the * aforementioned values match the given search term, the person is included. If not, the person is * excluded. * - * @param people The people through which we need to filter. + * @param roles The people through which we need to filter. * @param searchTerm The original search term that was typed by the user. * @return The filtered list of people (this could be empty). */ - public List<PersonSummaryDTO> studentsMatchingFilter(List<PersonSummaryDTO> people, + public List<RolePersonDetailsDTO> rolesMatchingFilter(List<RolePersonDetailsDTO> roles, String searchTerm) { var search = searchTerm.toLowerCase(); - return people.stream() - .filter(person -> person.getDisplayName() != null - && StringUtils.stripAccents(person.getDisplayName()) + return roles.stream() + .filter(r -> r.getPerson().getDisplayName() != null + && StringUtils.stripAccents(r.getPerson().getDisplayName()) .toLowerCase().contains(search) || - person.getUsername() != null && StringUtils.stripAccents(person.getUsername()) - .toLowerCase().contains(search) + r.getPerson().getUsername() != null + && StringUtils.stripAccents(r.getPerson().getUsername()) + .toLowerCase().contains(search) || - Objects.toString(person.getNumber()).contains(search)) - .collect(Collectors.toList()); + Objects.toString(r.getPerson().getNumber()).contains(search)) + .toList(); } /** @@ -156,19 +158,21 @@ public class EditionService { * Add participants according a CSV file to a specific editions. * * @param csv CSF file containing netid's and roles. - * @param edition The edition to which these people need to be added. + * @param editionId The edition id to which these people need to be added. + * @return The list of not found users * @throws IOException */ - public void addCourseParticipants(MultipartFile csv, EditionDetailsDTO edition) + public List<String> addCourseParticipants(MultipartFile csv, Long editionId) throws EmptyCsvException, InvalidCsvException { - List<UserCsvHelper> users = UserCsvHelper.readCsv(csv); - - for (UserCsvHelper csvUser : users) { - pApi.getPersonByUsername(csvUser.getNetId()) - .flatMap(person -> rApi.addRole(csvUser.person(new PersonIdDTO().id(person.getId())) - .edition(new EditionIdDTO().id(edition.getId())))) - .block(); - } + Map<String, ParticipantsImportDTO.InnerEnum> users = UserCsvHelper.readCsv(csv).stream() + .collect(toMap(UserCsvHelper::getIdentifier, UserCsvHelper::getRole)); + PersonSearchDTO result = pApi.searchForPeople(new ArrayList<>(users.keySet())).block(); + Map<String, ParticipantsImportDTO.InnerEnum> roleMap = result.getPeople().stream() + .collect(toMap(PersonSummaryDTO::getUsername, p -> users.getOrDefault(p.getUsername(), + users.getOrDefault(p.getEmail(), users.get(p.getNumber().toString()))))); + eca.addParticipantsToEdition(editionId, new ParticipantsImportDTO().participants(roleMap), false) + .block(); + return result.getNotFound(); } /** diff --git a/src/main/java/nl/tudelft/queue/service/EditionStatusService.java b/src/main/java/nl/tudelft/queue/service/EditionStatusService.java index b2163bb56226d3903ea748a0ea772b49d82b88e6..29227c3b90efc706a2929745941020a248a71f00 100644 --- a/src/main/java/nl/tudelft/queue/service/EditionStatusService.java +++ b/src/main/java/nl/tudelft/queue/service/EditionStatusService.java @@ -29,10 +29,10 @@ import java.util.stream.StreamSupport; import nl.tudelft.labracore.api.dto.AssignmentDetailsDTO; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; import nl.tudelft.labracore.api.dto.RoomDetailsDTO; -import nl.tudelft.queue.cache.AssignmentCacheManager; -import nl.tudelft.queue.cache.PersonCacheManager; -import nl.tudelft.queue.cache.RoomCacheManager; -import nl.tudelft.queue.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.cache.AssignmentCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.RoomCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.model.QLabRequest; import nl.tudelft.queue.model.enums.RequestType; diff --git a/src/main/java/nl/tudelft/queue/service/FeedbackService.java b/src/main/java/nl/tudelft/queue/service/FeedbackService.java index dab0b51811f6e6f36c2b9f3f1d0555acd5dbd581..9df09b16e00305a965f8571ae65366cc3cdd5a44 100644 --- a/src/main/java/nl/tudelft/queue/service/FeedbackService.java +++ b/src/main/java/nl/tudelft/queue/service/FeedbackService.java @@ -26,7 +26,7 @@ import javax.transaction.Transactional; import javax.validation.ValidationException; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; -import nl.tudelft.queue.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.queue.dto.patch.FeedbackPatchDTO; import nl.tudelft.queue.model.Feedback; import nl.tudelft.queue.model.LabRequest; diff --git a/src/main/java/nl/tudelft/queue/service/JitsiService.java b/src/main/java/nl/tudelft/queue/service/JitsiService.java index 9bd2d5990803272eb10f718561d8ec97c369a6b4..5d36e6ef6cbd2da25ae7d2d77d196b43c6d82db1 100644 --- a/src/main/java/nl/tudelft/queue/service/JitsiService.java +++ b/src/main/java/nl/tudelft/queue/service/JitsiService.java @@ -19,7 +19,7 @@ package nl.tudelft.queue.service; import java.util.UUID; -import nl.tudelft.queue.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.properties.JitsiProperties; diff --git a/src/main/java/nl/tudelft/queue/service/LabService.java b/src/main/java/nl/tudelft/queue/service/LabService.java index 3e4a2d972ad2370198af4cf17cf8b66c176986f8..0ae2f69e12906d52e9597e3cead719c922f782a7 100644 --- a/src/main/java/nl/tudelft/queue/service/LabService.java +++ b/src/main/java/nl/tudelft/queue/service/LabService.java @@ -23,7 +23,6 @@ import static nl.tudelft.queue.misc.QueueSessionStatus.*; import java.io.IOException; import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -34,11 +33,11 @@ import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.ModuleControllerApi; import nl.tudelft.labracore.api.SessionControllerApi; import nl.tudelft.labracore.api.dto.*; -import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.labracore.lib.cache.EditionCacheManager; +import nl.tudelft.labracore.lib.cache.EditionCollectionCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.data.Person; 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.dto.create.QueueSessionCreateDTO; import nl.tudelft.queue.dto.create.labs.AbstractSlottedLabCreateDTO; @@ -249,15 +248,15 @@ public class LabService { } @Transactional - public <D extends QueueSession<?>> List<D> createSessions(QueueSessionCreateDTO<D> dto, Long ouId, + public <D extends QueueSession<?>> List<D> createSessions(QueueSessionCreateDTO<D> dto, SessionType sessionType) { ArrayList<D> sessions = new ArrayList<>(); - sessions.add(createOneSession(dto, ouId, sessionType)); + sessions.add(createOneSession(dto, dto.getOrganisationalUnit(), sessionType)); if (dto.getRepeatForXWeeks() != null && dto.getRepeatForXWeeks() > 0) { for (int i = 0; i < dto.getRepeatForXWeeks(); i++) { dto.plusWeeks(1L); - sessions.add(createOneSession(dto, ouId, sessionType)); + sessions.add(createOneSession(dto, dto.getOrganisationalUnit(), sessionType)); } } @@ -317,20 +316,21 @@ public class LabService { * @param session The lab that is to be edited. */ @Transactional - public <D extends QueueSession<?>> void updateSession(QueueSessionPatchDTO<D> dto, D session) { + public <D extends QueueSession<?>> void updateSession(QueueSessionPatchDTO<D> dto) { // Get the assignments and rooms as ID DTOs. List<AssignmentIdDTO> assignments = dto.assignmentIdDTOs(); List<RoomIdDTO> rooms = dto.roomIdDTOs(); + @SuppressWarnings("unchecked") D qSession = (D) qsr.findByIdOrThrow(dto.getId()); // Patch the session on the Labracore side of things. - sApi.patchSession(session.getSession(), new SessionPatchDTO() + sApi.patchSession(qSession.getSession(), new SessionPatchDTO() .name(dto.getName()) .start((dto.getSlot() != null) ? dto.getSlot().getOpensAt() : null) .end((dto.getSlot() != null) ? dto.getSlot().getClosesAt() : null) .assignments(assignments) .rooms(rooms)).block(); - session.setExtraInfo(dto.getExtraInfo()); - dto.apply(session); + qSession.setExtraInfo(dto.getExtraInfo()); + dto.apply(qSession); } public void updateCapacitySessionRequests(CapacitySession session, @@ -477,11 +477,11 @@ public class LabService { var collection = ecCache.getOrThrow(sessionDetails.getEditionCollection().getId()); var roles = collection.getEditions().stream().map(e -> rService.rolesForPersonInEdition(e, assistant)).flatMap(List::stream).filter(rService::isStaff).toList(); - allowedAssignments = roles.stream() - .map(r -> mApi.getModuleByEdition(r.getEdition().getId()).collectList().block()) - .filter(Objects::nonNull) - .flatMap(Collection::stream).map(ModuleDetailsDTO::getAssignments) - .flatMap(List::stream).collect(Collectors.toList()); + // allowedAssignments = roles.stream() + // .map(r -> mApi.getModuleByEdition(r.getEdition().getId()).collectList().block()) + // .filter(Objects::nonNull) + // .flatMap(Collection::stream).map(ModuleDetailsDTO::getAssignments) + // .flatMap(List::stream).collect(Collectors.toList()); } return allowedAssignments; } diff --git a/src/main/java/nl/tudelft/queue/service/MailService.java b/src/main/java/nl/tudelft/queue/service/MailService.java index 359a63237975791afb6d29505b84e0b87e45d34e..3f446246c17756f852296b61b48fbac736770756 100644 --- a/src/main/java/nl/tudelft/queue/service/MailService.java +++ b/src/main/java/nl/tudelft/queue/service/MailService.java @@ -17,7 +17,7 @@ */ package nl.tudelft.queue.service; -import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.queue.dto.create.CourseRequestCreateDTO; import nl.tudelft.queue.properties.MailProperties; import nl.tudelft.queue.properties.QueueProperties; diff --git a/src/main/java/nl/tudelft/queue/service/PermissionService.java b/src/main/java/nl/tudelft/queue/service/PermissionService.java index bdc8d156162c1bf148afdb7dd43e11e5e89014ac..01b2b7b68c15a1b7e46673544b5094190dc2a3bf 100644 --- a/src/main/java/nl/tudelft/queue/service/PermissionService.java +++ b/src/main/java/nl/tudelft/queue/service/PermissionService.java @@ -28,11 +28,12 @@ import java.util.stream.Collectors; import nl.tudelft.labracore.api.AuthorizationControllerApi; import nl.tudelft.labracore.api.CourseControllerApi; +import nl.tudelft.labracore.api.StudentGroupControllerApi; import nl.tudelft.labracore.api.dto.*; +import nl.tudelft.labracore.lib.api.DefaultRole; +import nl.tudelft.labracore.lib.cache.*; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.LabradorUserDetails; -import nl.tudelft.labracore.lib.security.user.DefaultRole; -import nl.tudelft.labracore.lib.security.user.Person; -import nl.tudelft.queue.cache.*; import nl.tudelft.queue.model.*; import nl.tudelft.queue.model.enums.QueueSessionType; import nl.tudelft.queue.model.enums.RequestStatus; @@ -96,6 +97,9 @@ public class PermissionService { @Autowired private CourseControllerApi cApi; + @Autowired + private StudentGroupControllerApi sgApi; + /** * Gets the Person associated to the currently authenticated entity and passes it to the given callback. * If no API Key with user could be found, this function just outputs {@code false}. @@ -426,7 +430,8 @@ public class PermissionService { public boolean canLeaveEdition(Long editionId) { return withRole(editionId, (person, role) -> STAFF_ROLES.contains(role) || (role == STUDENT && withEdition(editionId, edition -> { - var groups = sgCache.getByPerson(person.getId()); + List<StudentGroupDetailsDTO> groups = sgCache.get(sgApi.getGroupsForPerson(person.getId()) + .map(StudentGroupSummaryDTO::getId).collectList().block()); var modules = edition.getModules().stream() .map(ModuleSummaryDTO::getId).collect(Collectors.toSet()); @@ -494,6 +499,14 @@ public class PermissionService { return isAdmin() || withRole(editionId, (person, role) -> TEACHER == role); } + /** + * @param qSessionId The id of the session that the user wants to manage. + * @return Whether the authenticated user can change properties of the given session. + */ + public boolean canManageSession(Long qSessionId) { + return isAdmin() || withQueueSession(qSessionId, this::canManageSession); + } + /** * @param qSession The session that the user wants to manage. * @return Whether the authenticated user can change properties of the given session. @@ -592,6 +605,18 @@ public class PermissionService { return isAdmin() || hasManagerRole(editionId); } + /** + * @param editionCollectionId The id of the edition collection the user wants to manage. + * @return Whether the authenticated user can manage the edition collection with the + * given id. + */ + public boolean canManageSharedEdition(Long editionCollectionId) { + return isAdmin() || withEditionCollection(editionCollectionId, + ec -> withAnyRole( + ec.getEditions().stream().map(EditionSummaryDTO::getId).collect(Collectors.toList()), + (person, role) -> role == TEACHER)); + } + /** * @param editionCollectionId The id of the edition collection the user wants to manage sessions for. * @return Whether the authenticated user can add/remove/change sessions within the @@ -604,6 +629,18 @@ public class PermissionService { (person, role) -> MANAGER_ROLES.contains(role))); } + /** + * @param editionCollectionId The id of the edition collection the user wants to manage participants for. + * @return Whether the authenticated user can add/remove/change participants within + * the edition collection with the given id. + */ + public boolean canManageSharedParticipants(Long editionCollectionId) { + return isAdmin() || withEditionCollection(editionCollectionId, + ec -> withAnyRole( + ec.getEditions().stream().map(EditionSummaryDTO::getId).collect(Collectors.toList()), + (person, role) -> MANAGER_ROLES.contains(role))); + } + /** * @param timeSlot The time slot the user wants to take a next request from. * @return Whether the authenticated user can take a next request from the given time slot. diff --git a/src/main/java/nl/tudelft/queue/service/QueuePushService.java b/src/main/java/nl/tudelft/queue/service/QueuePushService.java index b5861dedc2fc35e52f88aafd8daed8d05202a5f1..7ac1b29b336712d1c84c0ad5b241dd3af644ca1a 100644 --- a/src/main/java/nl/tudelft/queue/service/QueuePushService.java +++ b/src/main/java/nl/tudelft/queue/service/QueuePushService.java @@ -25,7 +25,7 @@ import lombok.SneakyThrows; import nl.martijndwars.webpush.Notification; import nl.martijndwars.webpush.PushService; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; -import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.realtime.NotificationPayload; import nl.tudelft.queue.realtime.Subscription; diff --git a/src/main/java/nl/tudelft/queue/service/RequestService.java b/src/main/java/nl/tudelft/queue/service/RequestService.java index 6bc340bbbe2f09ff06e19f2509a7c304c5eb6a8b..b86deb2a89edb1534ca8ea94a468c0bb9e413426 100644 --- a/src/main/java/nl/tudelft/queue/service/RequestService.java +++ b/src/main/java/nl/tudelft/queue/service/RequestService.java @@ -26,8 +26,8 @@ import nl.tudelft.labracore.api.ModuleControllerApi; import nl.tudelft.labracore.api.QuestionControllerApi; 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.*; +import nl.tudelft.labracore.lib.cache.*; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.queue.dto.create.RequestCreateDTO; import nl.tudelft.queue.dto.util.RequestTableFilterDTO; import nl.tudelft.queue.model.ClosableTimeSlot; diff --git a/src/main/java/nl/tudelft/queue/service/RequestTableService.java b/src/main/java/nl/tudelft/queue/service/RequestTableService.java index 29e49ba429f28150cacbc1ee4b3d942500309551..29887a324b1785372eae771ea7ac6bb73298cc28 100644 --- a/src/main/java/nl/tudelft/queue/service/RequestTableService.java +++ b/src/main/java/nl/tudelft/queue/service/RequestTableService.java @@ -28,9 +28,9 @@ import javax.servlet.http.HttpSession; import nl.tudelft.labracore.api.EditionControllerApi; import nl.tudelft.labracore.api.SessionControllerApi; import nl.tudelft.labracore.api.dto.*; -import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.labracore.lib.cache.*; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.librador.dto.view.View; -import nl.tudelft.queue.cache.*; import nl.tudelft.queue.dto.util.RequestTableFilterDTO; import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO; import nl.tudelft.queue.dto.view.RequestViewDTO; diff --git a/src/main/java/nl/tudelft/queue/service/RoleDTOService.java b/src/main/java/nl/tudelft/queue/service/RoleDTOService.java index 0fb97e106c57afefa27548b9a928b0c5b7de0d48..7c2f5114c202029495cfbf131a442e4ac3ebdd50 100644 --- a/src/main/java/nl/tudelft/queue/service/RoleDTOService.java +++ b/src/main/java/nl/tudelft/queue/service/RoleDTOService.java @@ -26,8 +26,8 @@ import java.util.stream.Collectors; import nl.tudelft.labracore.api.RoleControllerApi; import nl.tudelft.labracore.api.dto.*; -import nl.tudelft.labracore.lib.security.user.Person; -import nl.tudelft.queue.cache.EditionRolesCacheManager; +import nl.tudelft.labracore.lib.cache.EditionRolesCacheManager; +import nl.tudelft.labracore.lib.data.Person; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; diff --git a/src/main/resources/application.yml.template b/src/main/resources/application.yml.template index 5dbcc5abbe79af0f65b490ff633e66cc223f8df7..1b967f53bc7d5357830e638b76e5f945d9ca329e 100644 --- a/src/main/resources/application.yml.template +++ b/src/main/resources/application.yml.template @@ -50,7 +50,10 @@ spring: username: password: test-connection: true - + mvc: + hiddenmethod: + filter: + enabled: true opentracing: jaeger: diff --git a/src/main/resources/scss/_variables.scss b/src/main/resources/archive/_variables.scss similarity index 100% rename from src/main/resources/scss/_variables.scss rename to src/main/resources/archive/_variables.scss diff --git a/src/main/resources/templates/admin/announcements.html b/src/main/resources/archive/admin/announcements.html similarity index 100% rename from src/main/resources/templates/admin/announcements.html rename to src/main/resources/archive/admin/announcements.html diff --git a/src/main/resources/templates/admin/create/announcement.html b/src/main/resources/archive/admin/create/announcement.html similarity index 100% rename from src/main/resources/templates/admin/create/announcement.html rename to src/main/resources/archive/admin/create/announcement.html diff --git a/src/main/resources/templates/admin/create/course.html b/src/main/resources/archive/admin/create/course.html similarity index 100% rename from src/main/resources/templates/admin/create/course.html rename to src/main/resources/archive/admin/create/course.html diff --git a/src/main/resources/templates/admin/view.html b/src/main/resources/archive/admin/view.html similarity index 100% rename from src/main/resources/templates/admin/view.html rename to src/main/resources/archive/admin/view.html diff --git a/src/main/resources/templates/admin/view/calendar.html b/src/main/resources/archive/admin/view/calendar.html similarity index 100% rename from src/main/resources/templates/admin/view/calendar.html rename to src/main/resources/archive/admin/view/calendar.html diff --git a/src/main/resources/templates/admin/view/course.html b/src/main/resources/archive/admin/view/course.html similarity index 100% rename from src/main/resources/templates/admin/view/course.html rename to src/main/resources/archive/admin/view/course.html diff --git a/src/main/resources/templates/admin/view/courseList.html b/src/main/resources/archive/admin/view/courseList.html similarity index 100% rename from src/main/resources/templates/admin/view/courseList.html rename to src/main/resources/archive/admin/view/courseList.html diff --git a/src/main/resources/archive/assignment/create.html b/src/main/resources/archive/assignment/create.html new file mode 100644 index 0000000000000000000000000000000000000000..eae1b1452609bd975860036fd8259dffad4cdd27 --- /dev/null +++ b/src/main/resources/archive/assignment/create.html @@ -0,0 +1,102 @@ +<!-- + + 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}"> +<head> + <title th:text="|Create Assignment for ${'#' + edition.id}|"></title> + + <script src="/webjars/momentjs/min/moment.min.js"></script> + + <script type="application/javascript" + src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script> + <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/> + + <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script> + <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/> +</head> + +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + +<!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleSummaryDTO"--> + +<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueAssignmentCreateDTO"--> + +<body> +<section layout:fragment="subcontent"> + <div class="page-header"> + <h3>Create Assignment</h3> + </div> + + <form th:with="action = @{/module/{mId}/assignment/create(id=${edition.id}, mId=${_module.id})}" + th:action="${action}" th:object="${dto}" + class="form-horizontal" method="post"> + <input type="hidden" name="moduleId" th:value="${_module.id}"> + + <div class="form-group form-row"> + <label for="name-input" class="col-sm-2 col-form-label">Name:</label> + <div class="col-sm-4"> + <input id="name-input" + type="text" class="form-control" + required th:field="*{name}" + placeholder="The name of the assignment"/> + </div> + </div> + + <div class="form-group form-row"> + <label for="description-input" class="col-sm-2 col-form-label">Description:</label> + <div class="col-sm-10"> + <input id="description-input" + type="text" class="form-control" + th:field="*{description}" + placeholder="(Optionally) a description of the assignment"/> + </div> + </div> + + <div class="form-group form-row"> + <label for="deadline-input" class="col-sm-2 col-form-label">Deadline:</label> + <div class="col-sm-10 input-group" id="deadline-input" data-target-input="nearest"> + <input type="text" th:field="*{deadline}" + class="form-control datetimepicker-input" + data-target="#deadline-input"/> + <div class="input-group-append"> + <span class="input-group-text" + data-target="#deadline-input" + data-toggle="datetimepicker"> + <i class="fa fa-calendar"></i> + </span> + </div> + </div> + </div> + + <div class="form-group"> + <button type="submit" class="btn btn-primary ctrl-enter-submit float-right"> + Create new Assignment + </button> + <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> + </div> + </form> + + <script type="application/javascript"> + $("#deadline-input").datetimepicker({format: "DD-MM-YYYY HH:mm"}) + </script> +</section> +</body> +</html> diff --git a/src/main/resources/archive/assignment/remove.html b/src/main/resources/archive/assignment/remove.html new file mode 100644 index 0000000000000000000000000000000000000000..e08c73676030e1508252021595325097b134647e --- /dev/null +++ b/src/main/resources/archive/assignment/remove.html @@ -0,0 +1,47 @@ +<!-- + + 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="assignment" type="nl.tudelft.labracore.api.dto.AssignmentDetailsDTO"--> + +<body> +<section layout:fragment="subcontent"> + <div class="page-sub-header"> + <h3>Remove Assignment</h3> + </div> + + <form th:action="@{/assignment/{id}/remove(id=${assignment.id})}" + class="form-horizontal" + method="post"> + Are you sure you want to remove <strong + th:text="${'assignment #' + assignment.id + ' (' + assignment.name + ')'}"></strong>? + <div class="text-center"> + <button class="btn btn-danger">Delete this Assignment</button> + <small>or <a + th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small> + </div> + </form> +</section> +</body> +</html> diff --git a/src/main/resources/templates/course/request-submitted.html b/src/main/resources/archive/course/request-submitted.html similarity index 100% rename from src/main/resources/templates/course/request-submitted.html rename to src/main/resources/archive/course/request-submitted.html diff --git a/src/main/resources/archive/course/request.html b/src/main/resources/archive/course/request.html new file mode 100644 index 0000000000000000000000000000000000000000..09b23795143423f8b2910104234838c019a20254 --- /dev/null +++ b/src/main/resources/archive/course/request.html @@ -0,0 +1,86 @@ +<!-- + + 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="~{layout}"> +<head> + <title>Request Course</title> +</head> + +<!--@thymesVar id="editions" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"--> + +<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.CourseRequestCreateDTO"--> + +<body> +<section layout:fragment="content"> + <nav role="navigation" class="breadcrumbs"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li> + <li class="breadcrumb-item"><a th:href="@{/editions}">Courses</a></li> + <li class="breadcrumb-item active" aria-current="page">Request Course</li> + </ol> + </nav> + + <form th:action="@{/editions/request-course}" + th:object="${dto}" + class="form-horizontal" method="post"> + <div class="form-group form-row"> + <label for="name-input" class="col-sm-2 col-form-label">Name:</label> + <div class="col-sm-10"> + <input id="name-input" + type="text" class="form-control" + required th:field="*{name}" + placeholder="The full name of the course"/> + </div> + </div> + + <div class="form-group form-row"> + <label for="code-input" class="col-sm-2 col-form-label">Code:</label> + <div class="col-sm-10"> + <input id="code-input" + type="text" class="form-control" + required th:field="*{code}" + placeholder="The course code (i.e. IT1100)"/> + </div> + </div> + + <div class="form-group form-row"> + <label for="remarks-input" class="col-sm-2 col-form-label">Remarks:</label> + <div class="col-sm-10"> + <input id="remarks-input" + type="text" class="form-control" + th:field="*{remarks}" + placeholder="Any remarks you want to give regarding your course."/> + </div> + </div> + + <div class="form-group form-row"> + <div class="col-sm-offset-2 col-sm-8"> + <button type="submit" class="btn btn-primary ctrl-enter-submit"> + Request new Course + </button> + <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> + </div> + </div> + </form> +</section> +</body> + +</html> diff --git a/src/main/resources/static/js/create_tab.js b/src/main/resources/archive/create_tab.js similarity index 100% rename from src/main/resources/static/js/create_tab.js rename to src/main/resources/archive/create_tab.js diff --git a/src/main/resources/static/js/ctrl_enter_submit.js b/src/main/resources/archive/ctrl_enter_submit.js similarity index 100% rename from src/main/resources/static/js/ctrl_enter_submit.js rename to src/main/resources/archive/ctrl_enter_submit.js diff --git a/src/main/resources/archive/edition/create.html b/src/main/resources/archive/edition/create.html new file mode 100644 index 0000000000000000000000000000000000000000..a83ee67616caaec00a20d30407d93e7fe68aad9c --- /dev/null +++ b/src/main/resources/archive/edition/create.html @@ -0,0 +1,165 @@ +<!-- + + 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="~{admin/view}"> + +<!--@thymesVar id="edition" type="nl.tudelft.queue.dto.create.QueueEditionCreateDTO"--> + +<!--@thymesVar id="cohorts" type="java.util.List<nl.tudelft.labracore.api.dto.CohortSummaryDTO>"--> +<!--@thymesVar id="courses" type="java.util.List<nl.tudelft.labracore.api.dto.CourseSummaryDTO>"--> + +<head> + <script src="/webjars/momentjs/min/moment.min.js"></script> + + <script type="application/javascript" + src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script> + <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/> + + <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script> + <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/> +</head> + +<body> +<section layout:fragment="subcontent"> + <div class="page-sub-header"> + <h3>Create edition</h3> + </div> + + <form th:action="@{/edition/add}" + class="form-horizontal" + th:object="${edition}" + method="post" + enctype="multipart/form-data"> + <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> + + <div class="form-group text-danger" + th:if="${#lists.isEmpty(courses)}"> + You are not the manager of any course, please + <a th:href="@{/editions/request-course}"> + request a course first + </a>. + </div> + + <div class="form-group" + th:unless="${#lists.isEmpty(courses)}"> + Select the course under which you want to create a course edition. If your course is not in the list, or has + a different code, please + <a th:href="@{/editions/request-course}"> + request a course first + </a>. + </div> + + <div class="form-group"> + <label for="course-select">Select the course:</label> + <select th:field="*{course}" id="course-select" class="selectpicker col-md-6"> + <option value="none" disabled selected>Select course</option> + <option th:each="course : ${courses}" th:value="${course.id}" th:text="|${course.name} (${course.code})|"></option> + </select> + </div> + + <div class="form-group"> + <label for="edition-name">Edition name: (please name this after the year/semester, so for instance Q1 20/21, + this will be displayed in the course list as "[COURSE NAME] (Q1 20/21)"</label> + <input th:field="*{name}" type="text" id="edition-name" + placeholder="Edition name" class="form-control"/> + </div> + + <div class="form-group"> + Pick a cohort that you will teach during this edition. If you cannot find a fitting cohort, please select + one of the available cohorts. This will not impact any functionality for a standard course edition. + </div> + + <div class="form-group"> + <label for="cohort-select">Select the cohort:</label> + <select th:field="*{cohort}" id="cohort-select" class="selectpicker"> + <option value="none" disabled selected>Select cohort</option> + <option th:each="cohort : ${cohorts}" th:value="${cohort.id}" th:text="${cohort.name}"></option> + </select> + </div> + + <div class="form-group form-row"> + <div id="date-input" class="col-sm-10 row"> + <div class="col-sm-6 input-group" id="opens-at-input" data-target-input="nearest"> + <div class="input-group-prepend"> + <span class="input-group-text">From</span> + </div> + <input th:classappend="${#fields.hasErrors('startDate')} ? 'is-invalid'" + type="text" th:field="*{startDate}" + class="form-control datetimepicker-input" + data-target="#opens-at-input" required/> + <div class="input-group-append"> + <span class="input-group-text" + data-target="#opens-at-input" + data-toggle="datetimepicker"> + <i class="fa fa-calendar"></i> + </span> + </div> + <div class="invalid-feedback" th:if="${#fields.hasErrors('startDate')}" + th:errors="*{startDate}">Slot opensAt error + </div> + </div> + + <div class="col-sm-6 input-group date" id="closes-at-input" data-target-input="nearest"> + <div class="input-group-prepend"> + <span class="input-group-text">To</span> + </div> + <input th:classappend="${#fields.hasErrors('endDate')} ? 'is-invalid'" + type="text" th:field="*{endDate}" + class="form-control datetimepicker-input" + data-target="#closes-at-input" required/> + <div class="input-group-append"> + <span class="input-group-text" + data-target="#closes-at-input" + data-toggle="datetimepicker"> + <i class="fa fa-calendar"></i> + </span> + </div> + <div class="invalid-feedback" th:if="${#fields.hasErrors('endDate')}" + th:errors="*{endDate}">Slot error + </div> + </div> + </div> + </div> + + <div class="form-group"> + <div class="float-right"> + <input type="submit" class="btn btn-primary" value="Create edition"/> + </div> + </div> + </form> + + <!-- Invisible column for spacing at the bottom of the page --> + <div class="row mt-3"></div> + <script th:inline="javascript" type="text/javascript"> + //<![CDATA[ + + $(function () { + const opensAtInput = $("#opens-at-input"); + const closesAtInput = $("#closes-at-input"); + const timePickerInputs = [opensAtInput, closesAtInput]; + + timePickerInputs.forEach(input => input.datetimepicker({format: "DD-MM-YYYY HH:mm"})); + }) + //]]> + </script> +</section> +</body> +</html> diff --git a/src/main/resources/templates/edition/create/participant.html b/src/main/resources/archive/edition/create/participant.html similarity index 100% rename from src/main/resources/templates/edition/create/participant.html rename to src/main/resources/archive/edition/create/participant.html diff --git a/src/main/resources/templates/edition/edit/question.html b/src/main/resources/archive/edition/edit/question.html similarity index 100% rename from src/main/resources/templates/edition/edit/question.html rename to src/main/resources/archive/edition/edit/question.html diff --git a/src/main/resources/archive/edition/enrol.html b/src/main/resources/archive/edition/enrol.html new file mode 100644 index 0000000000000000000000000000000000000000..7029e5f84a9a5955f715b1371334ab6a7ac6f383 --- /dev/null +++ b/src/main/resources/archive/edition/enrol.html @@ -0,0 +1,50 @@ +<!-- + + 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="~{layout}"> + +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + +<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"><a href="/editions">Courses</a></li> + <li class="breadcrumb-item"><a href="#" th:href="@{/edition/{id}/enrol(id=${edition.id})}" th:text="${edition.course.id}"></a></li> + <li class="breadcrumb-item active">Enrol</li> + </ol> + </nav> + + <div class="page-header"> + <h1>Enrol</h1> + </div> + + <p>Do you wish to enrol for the course edition <strong th:text="${edition.course.name}"></strong>?</p> + + <form th:action="@{/edition/{id}/enrol(id=${edition.id})}" class="form-horizontal" method="post"> + <div class="text-center"> + <button type="submit" name="enrol" class="btn btn-success">Enrol</button> + <small> or <a th:href="@{/editions}">go back</a></small> + </div> + </form> + </section> +</body> +</html> diff --git a/src/main/resources/archive/edition/index.html b/src/main/resources/archive/edition/index.html new file mode 100644 index 0000000000000000000000000000000000000000..7f2200039562cd3bd7e8d8571c91695e941a0261 --- /dev/null +++ b/src/main/resources/archive/edition/index.html @@ -0,0 +1,166 @@ +<!-- + + 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="~{layout}"> +<head> + <title>Editions</title> + + <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"/> +</head> + +<!--@thymesVar id="editions" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"--> + +<!--@thymesVar id="programs" type="java.util.List<nl.tudelft.labracore.api.dto.ProgramSummaryDTO>"--> + +<!--@thymesVar id="filter" type="nl.tudelft.queue.dto.util.EditionFilterDTO"--> + +<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 active" aria-current="page">Courses</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> + </th:block> + <h1>Course Editions</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"> + <select multiple class="selectpicker" size="10" + id="program-select" name="programs" + data-mobile="true" data-width="100%" + data-selected-text-format="count > 1"> + <th:block th:each="program : ${programs}"> + <option th:value="${program.id}" + th:selected="${(filter?.programs ?: {}).contains(program.id)}" + th:text="${program.name}"> + </option> + </th:block> + </select> + </div> + </div> + + <div class="form-group"> + <input class="form-control" name="nameSearch" th:value="${filter?.nameSearch ?: ''}"> + </div> + + <div class="form-group"> + <div class="col-sm-2"> + <button class="btn btn-primary" type="submit">Filter</button> + </div> + </div> + </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"> + <thead> + <tr> + <th>Edition</th> + <th>Code</th> + <th>Teacher</th> + <th></th> + </tr> + </thead> + <tbody> + <tr th:each="edition : ${editions}"> + <td><a 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"> + <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:block> + <th:block th:if="${@permissionService.canViewEdition(edition)}"> + <a class="btn btn-primary" + th:href="@{/edition/{id}(id=${edition.id})}">view</a> + </th:block> + </td> + </tr> + </tbody> + </table> + </div> + + <!-- Shown on small devices (phone/tablet) --> + <div class="d-block d-md-none"> + <div th:each="edition : ${editions}" class="card" + th:classappend="${@permissionService.canViewEdition(edition)} ? 'bg-primary text-white' : 'bg-light'" + style="margin-bottom:20px;"> + <h4 class="card-header text-dark" th:text="|${edition.course.name} (${edition.name})|"></h4> + + <div class="card-body"> + <dl> + <dt class="text-dark">Teacher</dt> + <dd class="truncate text-dark" + th:text="${#strings.listJoin(@roleDTOService.teacherNames(edition), ', ')}"></dd> + </dl> + + </div> + <div class="card-footer"> + <div class="btn-group btn-group-justified"> + <th:block th:if="${@permissionService.canViewEdition(edition)}"> + <a th:href="@{/edition/{id}(id=${edition.id})}" + class="btn btn-primary btn-sm">View</a> + </th:block> + + <th:block th:unless="${@permissionService.canViewEdition(edition)}"> + <a th:href="@{/edition/{id}/enrol(id=${edition.id})}" + class="btn btn-success btn-sm">Enrol</a> + </th:block> + </div> + </div> + </div> + </div> + + <nav th:include="pagination :: pagination (page=${editions}, size=3)"></nav> +</section> +</body> + +</html> diff --git a/src/main/resources/templates/edition/remove/participant.html b/src/main/resources/archive/edition/remove/participant.html similarity index 100% rename from src/main/resources/templates/edition/remove/participant.html rename to src/main/resources/archive/edition/remove/participant.html diff --git a/src/main/resources/templates/edition/remove/question.html b/src/main/resources/archive/edition/remove/question.html similarity index 100% rename from src/main/resources/templates/edition/remove/question.html rename to src/main/resources/archive/edition/remove/question.html diff --git a/src/main/resources/archive/edition/view.html b/src/main/resources/archive/edition/view.html new file mode 100644 index 0000000000000000000000000000000000000000..67309819aa9cd76ff3fa7f836b0b421ee089c3ac --- /dev/null +++ b/src/main/resources/archive/edition/view.html @@ -0,0 +1,107 @@ +<!-- + + 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="~{layout}"> + +<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"--> +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + +<!--@thymesVar id="message" type="java.lang.String"--> + +<body> +<section layout:fragment="content"> + <section layout:fragment="breadcrumb"> + <nav role="navigation" class="breadcrumbs"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li> + <li class="breadcrumb-item"> + <a th:if="${edition != null && @permissionService.canEnrolForEdition(edition.id)}" + th:href="@{/editions}">Catalog</a> + <a th:unless="${edition != null && @permissionService.canEnrolForEdition(edition.id)}" + th:href="@{/}">My Courses</a> + </li> + <li class="breadcrumb-item active" th:if="${ec == null}" th:text="${edition.course.code}">TI1316</li> + <li class="breadcrumb-item active" th:unless="${ec == null}" th:text="${ec.name}">TI1316</li> + </ol> + </nav> + </section> + + <ul class="nav nav-tabs" th:if="${ec == null}"> + <li class="nav-item"> + <a href="#" class="nav-link" + th:classappend="${#request.requestURI.matches('.*/edition/\d+') ? 'active' : ''}" + th:href="@{/edition/{id}(id=${edition.id})}"> + <i class="fa fa-th-large" aria-hidden="true"></i> Info + </a> + </li> + <li class="nav-item" th:if="${@permissionService.canManageParticipants(edition.id)}"> + <a href="#" class="nav-link" + th:classappend="${#request.requestURI.matches('.*/participants.*') ? 'active' : ''}" + th:href="@{/edition/{id}/participants(id=${edition.id})}"> + <i class="fa fa-user" aria-hidden="true"></i> Participants + </a> + </li> + <li class="nav-item" th:if="${@permissionService.canViewEdition(edition.id)}"> + <a href="#" class="nav-link" + th:classappend="${#request.requestURI.matches('.*/modules.*') ? 'active' : ''}" + th:href="@{/edition/{id}/modules(id=${edition.id})}"> + <i class="fa fa-boxes" aria-hidden="true"></i> Modules + </a> + </li> + <li class="nav-item" th:if="${@permissionService.canViewEdition(edition.id)}"> + <a href="#" class="nav-link" + th:classappend="${#request.requestURI.matches('.*/labs.*') ? 'active' : ''}" + th:href="@{/edition/{id}/labs(id=${edition.id})}"> + <i class="fa fa-calendar" aria-hidden="true"></i> Labs + </a> + </li> + <li class="nav-item" th:if="${@permissionService.canManageAssignments(edition.id)}"> + <a href="#" class="nav-link" + th:classappend="${#request.requestURI.matches('.*/status.*') ? 'active' : ''}" + th:href="@{/edition/{id}/status(id=${edition.id})}"> + <i class="fa fa-bar-chart" aria-hidden="true"></i> Status + </a> + </li> + <li class="nav-item" th:if="${@permissionService.canManageQuestions(edition.id)}"> + <a href="#" class="nav-link" + th:classappend="${#request.requestURI.matches('.*/questions.*') ? 'active' : ''}" + th:href="@{/edition/{id}/questions(id=${edition.id})}"> + <i class="fa fa-question" aria-hidden="true"></i> Questions + </a> + </li> + </ul> + + <div class="alert alert-info mt-md-3" role="alert" th:unless="${#strings.isEmpty(message)}"> + <th:block th:text="${message}"> + </th:block> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + + <div class="pb-3"> + <div layout:fragment="subcontent"> + Sub content page + </div> + </div> +</section> +</body> +</html> diff --git a/src/main/resources/templates/edition/view/info.html b/src/main/resources/archive/edition/view/info.html similarity index 100% rename from src/main/resources/templates/edition/view/info.html rename to src/main/resources/archive/edition/view/info.html diff --git a/src/main/resources/archive/edition/view/labs.html b/src/main/resources/archive/edition/view/labs.html new file mode 100644 index 0000000000000000000000000000000000000000..30d50704ec5ca7269cbc51ef6e73ae08d34f1235 --- /dev/null +++ b/src/main/resources/archive/edition/view/labs.html @@ -0,0 +1,157 @@ +<!-- + + 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="labs" type="java.util.List<nl.tudelft.queue.dto.view.QueueSessionSummaryDTO>"--> +<!--@thymesVar id="allLabTypes" type="nl.tudelft.queue.models.enums.LabType[]"--> +<!--@thymesVar id="queueSessionTypes" type="java.util.List<nl.tudelft.queue.model.enums.QueueSessionType>"--> +<!--@thymesVar id="allModules" type="java.util.List<nl.tudelft.labracore.dto.view.structured.summary.ModuleSummaryDTO>"--> +<!--@thymesVar id="modules" type="java.util.List<Long>"--> + +<!--@thymesVar id="alert" type="java.lang.String"--> + +<head> + <title th:text="|Labs - ${edition.course.name} (${edition.name})|"></title> + + <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"/> +</head> + +<body> +<section layout:fragment="subcontent"> + + <div class="alert alert-danger mt-md-3" role="alert" th:unless="${#strings.isEmpty(alert)}" + th:text="${alert}"> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + + <div class="page-sub-header"> + <div class="float-right" th:if="${@permissionService.canManageSessions(edition.id)}"> + <a th:each="lType : ${T(nl.tudelft.queue.model.enums.QueueSessionType).values()}" + href="#" th:href="@{/edition/{id}/lab/create(id=${edition.id}, type=${lType.name()})}" + th:text="|Create ${lType.displayName} Lab|" + class="btn btn-primary mr-1"> + </a> + </div> + <h3>Labs</h3> + </div> + + <th:block th:unless="${#lists.isEmpty(labs) and #lists.isEmpty(queueSessionTypes) and #lists.isEmpty(modules)}"> + <form id="filter-form" method="get" class="form-inline mb-3" + th:action="@{/edition/{id}/labs(id=${edition.id})}"> + <div class="container-fluid"> + <div class="row align-items-end"> + <div class="form-group col-sm-2"> + <label class="form-control-label" for="lab-type-select">Type</label> + <select multiple class="form-control selectpicker" data-width="100%" th:name="queueSessionTypes" id="lab-type-select"> + <th:block th:each="labType : ${allLabTypes}"> + <option th:value="${labType}" + th:text="${labType.displayName}" + th:selected="${queueSessionTypes.contains(labType)}"> + </option> + </th:block> + </select> + </div> + <div class="form-group col-sm-2"> + <label class="form-control-label" for="module-select">Module</label> + <select multiple class="form-control selectpicker" data-width="100%" th:name="modules" id="module-select"> + <th:block th:each="module : ${allModules}"> + <option th:value="${module.id}" + th:text="${module.name}" + th:selected="${modules.contains(module.id)}"> + </option> + </th:block> + </select> + </div> + <div class="col-auto"> + <button type="submit" class="btn btn-primary"> + Filter + </button> + <a th:unless="${#lists.isEmpty(queueSessionTypes) and #lists.isEmpty(modules)}" + class="btn" + th:href="@{/edition/{id}/labs(id=${edition.id})}"> + <i class="fa fa-times" aria-hidden="true"></i> Clear current filters + </a> + </div> + </div> + </div> + </form> + </th:block> + + <th:block th:if="${#lists.isEmpty(labs)}"> + No labs for this edition. + </th:block> + + <th:block th:unless="${#lists.isEmpty(labs)}"> + <ul class="list-group sort-open"> + <th:block th:each="lab : ${labs}"> + <li class="list-group-item lab-item" + th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'"> + <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" + th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> + </a> + + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.open()}">Active</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.closed()}">Completed</span> + + <span class="badge badge-pill badge-danger text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> + <span class="badge badge-pill badge-warning text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> + + <span class="badge badge-pill badge-warning text-muted" + th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> + + <div class="btn-group float-right"> + <th:block th:if="${@permissionService.canManageSessions(edition.id)}"> + <a th:href="@{/lab/{id}/edit(id=${lab.id})}" class="btn btn-sm btn-secondary"> + <i class="fa fa-pencil" aria-hidden="true"></i> + </a> + </th:block> + <th:block th:if="${@permissionService.canManageSessions(edition.id)}"> + <a th:href="@{/lab/{id}/remove(id=${lab.id})}" + class="btn btn-sm btn-danger"> + <i class="fa fa-trash-o" aria-hidden="true"></i> + </a> + </th:block> + <th:block th:if="${@permissionService.canManageSessions(edition.id)}"> + <a href="#" + th:href="@{/lab/{labId}/copy(labId=${lab.id})}" + class="btn btn-sm btn-primary"> + <i class="fa fa-copy" aria-hidden="true"></i> + </a> + </th:block> + </div> + </li> + </th:block> + </ul> + </th:block> +</section> +</body> +</html> diff --git a/src/main/resources/templates/edition/view/leave.html b/src/main/resources/archive/edition/view/leave.html similarity index 100% rename from src/main/resources/templates/edition/view/leave.html rename to src/main/resources/archive/edition/view/leave.html diff --git a/src/main/resources/archive/edition/view/modules.html b/src/main/resources/archive/edition/view/modules.html new file mode 100644 index 0000000000000000000000000000000000000000..ebb30c39dec8e9dcb0f9272995a0212649f97784 --- /dev/null +++ b/src/main/resources/archive/edition/view/modules.html @@ -0,0 +1,159 @@ +<!-- + + 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="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"--> +<!--@thymesVar id="groups", type="java.util.Map<java.lang.Long, java.util.List<nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO>>"--> + +<head> + <title th:text="|Modules - ${edition.course.name} (${edition.name})|"></title> +</head> + +<body> +<section layout:fragment="subcontent"> + <div class="page-sub-header"> + <a class="btn btn-primary float-right" + th:href="@{/edition/{id}/modules/create(id=${edition.id})}" + th:if="${@permissionService.canManageModules(edition.id)}"> + Create Module + </a> + <h3>Modules</h3> + </div> + + <h5 th:if="${#lists.isEmpty(modules)}"> + No modules. + <th:block th:if="${@permissionService.canManageModules(edition.id)}"> + Click on the "Create Module" button to make the first module. + </th:block> + </h5> + + <div th:unless="${#lists.isEmpty(modules)}" class="row"> + <div class="col-md-3 mb-2"> + <div class="list-group" role="tablist"> + <th:block th:each="_module, idx : ${modules}"> + <a class="list-group-item list-group-item-action" + th:classappend="${idx.first ? 'active' : ''}" + role="tab" data-toggle="list" + th:href="'#module-' + ${_module.id}" th:text="${_module.name}"></a> + </th:block> + </div> + </div> + + <div class="col-md-9"> + <div class="tab-content"> + <div th:each="m, idx : ${modules}" + class="tab-pane fade" th:classappend="${idx.first ? 'active show' : ''}" + role="tabpanel" + th:id="'module-' + ${m.id}"> + + <div> + <a class="btn btn-danger btn-sm float-right" + th:if="${@permissionService.canManageModule(m.id)}" + th:href="@{/module/{mId}/remove(mId=${m.id})}" + th:text="|Remove module - ${m.name}|"> + </a> + </div> + + <div> + <a class="btn btn-primary btn-sm float-right mr-4" + th:if="${@permissionService.canManageModule(m.id)}" + th:href="@{/module/{mId}/assignment/create(mId=${m.id})}"> + Create Assignment + </a> + <h3 class="ml-2">Assignments</h3> + </div> + + <h5 th:if="${#lists.isEmpty(m.assignments)}"> + There are no assignments in this module. + </h5> + + <div class="row col-12 mt-2" th:unless="${#lists.isEmpty(m.assignments)}"> + <table class="table"> + <thead class="thead-light"> + <tr> + <th scope="col">#</th> + <th scope="col">Name</th> + <th scope="col">Description</th> + <th scope="col"></th> + </tr> + </thead> + <tr th:each="assignment, aIdx : ${m.getAssignments()}"> + <th scope="row" th:text="${assignment.id}"></th> + <td th:text="${assignment.name}"></td> + <td th:text="${assignment.description.text}"></td> + <td> + <a th:href="@{/assignment/{aId}/remove(aId=${assignment.id})}" + th:if="${@permissionService.canManageModule(m.id)}" + class="btn btn-danger float-right to-be-implemented"> + <i class="fa fa-trash"></i> + </a> + </td> + </tr> + </table> + </div> + + <th:block th:if="${@permissionService.canManageModule(m.id)}"> + <h3 class="ml-2">Student Groups</h3> + + <h5 th:if="${#lists.isEmpty(groups.get(m.id))}"> + There are no student groups in this module, configure them in Portal + </h5> + + <div class="row col-12" th:unless="${#lists.isEmpty(groups.get(m.id))}"> + <table class="table"> + <thead class="thead-light"> + <tr> + <th scope="col" class="fit-width">#</th> + <th scope="col">Name</th> + <th scope="col">Members</th> + <th scope="col" class="fit">Capacity</th> + </tr> + </thead> + <tr th:each="group, gIdx : ${groups.get(m.id)}" + th:with="students = ${@roleDTOService.studentsL1(group.members)}"> + <th class="fit-width" scope="row" th:text="${group.id}"></th> + <td th:text="${group.name}"></td> + <td th:text="${#strings.listJoin(@roleDTOService.names(students), ', ')}"></td> + <td class="fit"> + <span class="badge" + th:classappend="${group.capacity == students.size() ? 'badge-danger' : 'badge-primary'}" + th:text="|${group.capacity} / ${group.capacity}|"></span> + </td> + </tr> + </table> + </div> + </th:block> + </div> + </div> + </div> + </div> + + <script> + $(function () { + $("[data-toggle='tooltip']").tooltip() + }) + </script> +</section> +</body> +</html> diff --git a/src/main/resources/archive/edition/view/participants.html b/src/main/resources/archive/edition/view/participants.html new file mode 100644 index 0000000000000000000000000000000000000000..a786b2581d13a776dba4f38f94612ca5eb7e5803 --- /dev/null +++ b/src/main/resources/archive/edition/view/participants.html @@ -0,0 +1,199 @@ +<!-- + + 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="students" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.PersonSummaryDTO>"--> + +<head> + <title th:text="|Participants - ${edition.course.name} (${edition.name})|"></title> +</head> + +<body> +<section layout:fragment="subcontent" th:with="staffTabActive = ${#httpServletRequest.getParameter('staff-tab-active')}"> + <div class="page-sub-header"> + <div class="float-right" style="margin-right: 10px"> + <a href="#" th:href="@{/edition/{id}/participants/create(id=${edition.id})}" + class="btn btn-secondary"> + Add participant + </a> + </div> + <h3>Participants</h3> + </div> + + <div class="boxed-group"> + <ul class="nav nav-pills"> + <li role="presentation" th:if="${staffTabActive}" class="nav-item"> + <a class="nav-link" + th:href="@{/edition/{id}/participants(id=${edition.id}, staff-tab-active=false)}">Students</a> + </li> + <li role="presentation" th:unless="${staffTabActive}" class="nav-item"> + <a href="#" class="nav-link active">Students</a> + </li> + <li role="presentation" th:if="${staffTabActive}" class="nav-item"> + <a href="#" class="nav-link active">Staff</a> + </li> + <li role="presentation" th:unless="${staffTabActive}" class="nav-item"> + <a class="nav-link" + th:href="@{/edition/{id}/participants(id=${edition.id}, staff-tab-active=true)}">Staff</a> + </li> + </ul> + </div> + + <th:block th:if="${staffTabActive}"> + <div class="boxed-group" th:with="teachers = ${@roleDTOService.teachers(edition)}"> + <h3>Teachers</h3> + <div class="boxed-group-inner" + th:if="${#lists.isEmpty(teachers)}"> + There are no teachers participating in this edition. + </div> + + <ul class="list-group" + th:unless="${#lists.isEmpty(teachers)}"> + <li class="list-group-item" + th:each="person : ${teachers}" + th:text="${person.displayName}"> + <div class="float-right" + th:if="${@permissionService.canManageTeachers(edition.id)}"> + <a class="btn btn-xs btn-danger" + th:href="@{/edition/{editionId}/participants/{personId}/remove(editionId=${edition.id},personId=${person.id})}"> + <i class="fa fa-trash-o" aria-hidden="true"></i> + </a> + </div> + </li> + </ul> + </div> + + <div class="boxed-group" th:with="headTAs = ${@roleDTOService.headTAs(edition)}"> + <h3>Manager</h3> + <div class="boxed-group-inner" + th:if="${#lists.isEmpty(headTAs)}"> + There are no managers participating in this edition. + </div> + + <ul class="list-group" + th:unless="${#lists.isEmpty(headTAs)}"> + <li class="list-group-item" + th:each="person : ${headTAs}"> + <th:block th:if="${@permissionService.canViewFeedback(person.id)}"> + <a th:href="@{/feedback/{id}(id=${person.id})}" + th:text="${person.displayName}"></a> + </th:block> + <th:block th:unless="${@permissionService.canViewFeedback(person.id)}" + th:text="${person.displayName}"> + </th:block> + <div class="float-right"> + <a class="btn btn-xs btn-danger" + th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${person.id})}"> + <i class="fa fa-trash-o" aria-hidden="true"></i> + </a> + </div> + </li> + </ul> + </div> + + <div class="boxed-group" th:with="assistants = ${@roleDTOService.assistants(edition)}"> + <h3>Assistants</h3> + <div class="boxed-group-inner" + th:if="${#lists.isEmpty(assistants)}"> + There are no assistants participating in this edition. + </div> + + <ul class="list-group" + th:unless="${#lists.isEmpty(assistants)}"> + <li class="list-group-item" + th:each="person : ${assistants}"> + <th:block + th:if="${@permissionService.canViewFeedback(person.id)}"> + <a th:href="@{/feedback/{id}(id=${person.id})}" + th:text="${person.displayName}"></a> + <!-- <span th:text="'(' + ${feedbackCount.get(person.user.id)} + ')'" class="small">0</span>--> + </th:block> + <th:block th:unless="${@permissionService.canViewFeedback(person.id)}" + th:text="${person.displayName}"> + </th:block> + <div class="float-right"> + <a class="btn btn-xs btn-danger" + th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${person.id})}"> + <i class="fa fa-trash-o" aria-hidden="true"></i> + </a> + </div> + </li> + </ul> + </div> + </th:block> + + <th:block th:unless="${staffTabActive}"> + <form action="" class="form-horizontal" method="get"> + <div class="form-group"> + <label for="filter">Find a student: </label> + <input type="text" id="filter" class="form-control" name="student-search" placeholder="Student name" + th:value="${#httpServletRequest.getParameter('student-search')}"/> + </div> + </form> + + <div class="boxed-group"> + <h3>Students</h3> + <div class="boxed-group-inner" + th:if="${#lists.isEmpty(students)}"> + There are no students participating in this edition. + </div> + + <ul class="list-group" th:unless="${students.size == 0}"> + <li class="list-group-item" th:each="student : ${students}"> + <th:block th:if="${@permissionService.isAdminOrTeacher()}"> + <a th:href="@{/history/edition/{editionId}/student/{id}(editionId=${edition.id},id=${student.id})}" + th:text="${student.displayName}"></a> + </th:block> + <th:block th:unless="${@permissionService.isAdminOrTeacher()}" + th:text="${student.displayName}"> + </th:block> + <!-- Disabled at the moment because of referential constraint errors in labracore (groups) --> +<!-- <div class="float-right">--> +<!-- <a class="btn btn-xs btn-danger"--> +<!-- th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${student.id})}">--> +<!-- <i class="fa fa-trash-o" aria-hidden="true"></i>--> +<!-- </a>--> +<!-- </div>--> + </li> + </ul> + </div> + + <div th:if="${students != null && students.totalPages > 1}"> + <th:block th:replace="pagination :: pagination (page=${students}, size=3)"></th:block> + </div> + </th:block> + + <!-- <div th:if="${cleaned != null}">--> + <!-- <script th:inline="javascript">--> + <!-- /*<![CDATA[*/--> + <!-- var count = /*[[${cleaned}]]*/ '0';--> + <!-- var message = "Cleaned " + count + " users";--> + <!-- alert(message);--> + <!-- /*]]>*/--> + <!-- </script>--> + <!-- </div>--> + +</section> +</body> +</html> diff --git a/src/main/resources/archive/edition/view/questions.html b/src/main/resources/archive/edition/view/questions.html new file mode 100644 index 0000000000000000000000000000000000000000..043bfe32b473c65fbb2b48ab74e49197e0150c93 --- /dev/null +++ b/src/main/resources/archive/edition/view/questions.html @@ -0,0 +1,76 @@ +<!-- + + 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"--> + +<head> + <title th:text="|Questions - ${edition.course.name} (${edition.name})|"></title> +</head> + +<body> +<section layout:fragment="subcontent"> + + <div class="page-sub-header"> + <h3>Questions</h3> + </div> + + <th:block th:if="${#lists.isEmpty(questions)}"> + No questions for this edition. + </th:block> + + <th:block th:unless="${#lists.isEmpty(questions)}"> + <table class="table"> + <thead class="thead-light"> + <tr> + <th scope="col" class="fit-width">#</th> + <th scope="col">Question</th> + <th scope="col">Answer</th> + <th scope="col">Assignment</th> + <th scope="col">Asked at</th> + <th scope="col" class="fit"></th> + </tr> + </thead> + <tr th:each="question : ${questions}"> + <th class="fit-width" scope="row" th:text="${question.id}"></th> + <td th:text="${question.question}"></td> + <td th:text="${question.answer} ?: 'No answer'"></td> + <td th:text="${question.assignment?.name} ?: 'None'"></td> + <td th:text="${#temporals.format(question.askedAt, 'dd MMMM yyyy HH:mm')}"></td> + <td class="fit"> + <div class="btn-group float-right"> + <a th:href="@{/edition/{edition}/questions/{question}/edit(edition=${edition.id},question=${question.id})}" + class="btn btn-sm btn-secondary"> + <i class="fa fa-pencil" aria-hidden="true"></i> + </a> + <a th:href="@{/edition/{edition}/questions/{question}/remove(edition=${edition.id},question=${question.id})}" + class="btn btn-sm btn-danger"> + <i class="fa fa-trash-o" aria-hidden="true"></i> + </a> + </div> + </td> + </tr> + </table> + </th:block> + +</section> +</body> +</html> diff --git a/src/main/resources/templates/edition/view/status.html b/src/main/resources/archive/edition/view/status.html similarity index 100% rename from src/main/resources/templates/edition/view/status.html rename to src/main/resources/archive/edition/view/status.html diff --git a/src/main/resources/archive/error/400.html b/src/main/resources/archive/error/400.html new file mode 100644 index 0000000000000000000000000000000000000000..3e1e0f6fee5a9ca63fc7b0260910d51902abed8a --- /dev/null +++ b/src/main/resources/archive/error/400.html @@ -0,0 +1,39 @@ +<!-- + + 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout}"> +<head> + <title>Queue: Bad Request</title> +</head> + +<body> +<section layout:fragment="content"> + <div class="page-header"> + <h1>Error</h1> + </div> + + <h2>Bad Request</h2> + + Looks like a developer made a booboo. Please report what you did in the Queue Mattermost channel or + through e-mail to eip-ewi@tudelft.nl so we can quickly resolve the issue. +</section> +</body> +</html> diff --git a/src/main/resources/archive/error/403.html b/src/main/resources/archive/error/403.html new file mode 100644 index 0000000000000000000000000000000000000000..274119866f0f70e1c8aa10bd4b56db55d4bf443f --- /dev/null +++ b/src/main/resources/archive/error/403.html @@ -0,0 +1,41 @@ +<!-- + + 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout}"> +<head> + <title>Queue: Access denied</title> +</head> + +<body> + <section layout:fragment="content"> + <div class="page-header"> + <h1>Error</h1> + </div> + + <h2>Access Denied</h2> + + <p>You do not have permission to enter. This incident will be reported.</p> + + <img src="https://imgs.xkcd.com/comics/incident.png" alt="XKCD" /> + <p>Source: <a href="http://www.xkcd.org">xkcd</a></p> + </section> +</body> +</html> diff --git a/src/main/resources/archive/error/404.html b/src/main/resources/archive/error/404.html new file mode 100644 index 0000000000000000000000000000000000000000..54f19ec28f55d1928d83c159bc5748d6a71a74e1 --- /dev/null +++ b/src/main/resources/archive/error/404.html @@ -0,0 +1,34 @@ +<!-- + + 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> + +<body> +<section layout:fragment="content"> + <div class="page-header"> + <h1>Error 404</h1> + </div> + + <h2>Object not found</h2> + + <p>The requested object could not be found, stop looking for things that aren't there :)</p> +</section> +</body> +</html> diff --git a/src/main/resources/archive/error/422.html b/src/main/resources/archive/error/422.html new file mode 100644 index 0000000000000000000000000000000000000000..656d7a06df65a6595ef59bd225701a697fa739e9 --- /dev/null +++ b/src/main/resources/archive/error/422.html @@ -0,0 +1,38 @@ +<!-- + + 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}"> +<head> + <title>Queue: Error</title> +</head> + +<body> + <section layout:fragment="content"> + <div class="page-header"> + <h1>Error</h1> + </div> + <div class="jumbotron alert-danger"> + <h1>Something went wrong during validation of an entity.</h1> + <h5>Are you trying to break Queue?</h5> + </div> + </section> +</body> +</html> + diff --git a/src/main/resources/archive/error/500.html b/src/main/resources/archive/error/500.html new file mode 100644 index 0000000000000000000000000000000000000000..ce6bfb8b82800c99dda469904769966a4efa1934 --- /dev/null +++ b/src/main/resources/archive/error/500.html @@ -0,0 +1,41 @@ +<!-- + + 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}"> + +<head> + <title>Queue: Error</title> +</head> + +<body> + <section layout:fragment="content"> + <div class="page-header"> + <h1>Error</h1> + </div> + <div class="jumbotron alert-danger"> + <h1> + Oops. Something went wrong on our end...<br> + If this occurs again, contact us with a description of what you did before we show you this screen + </h1> + </div> + </section> +</body> +</html> + diff --git a/src/main/resources/archive/error/error.html b/src/main/resources/archive/error/error.html new file mode 100644 index 0000000000000000000000000000000000000000..36a0ad6eb251e88ee2524e55c1164ac4c095ef51 --- /dev/null +++ b/src/main/resources/archive/error/error.html @@ -0,0 +1,37 @@ +<!-- + + 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}"> +<head> + <title>Queue: Error</title> +</head> + +<body> + <section layout:fragment="content"> + <div class="page-header"> + <h1>Error</h1> + </div> + <div class="jumbotron alert-danger"> + <h1>Oops. Something went wrong...</h1> + </div> + </section> +</body> +</html> + diff --git a/src/main/resources/archive/global.js b/src/main/resources/archive/global.js new file mode 100644 index 0000000000000000000000000000000000000000..8709e847b73769aeea4890478d8c7df729a54fef --- /dev/null +++ b/src/main/resources/archive/global.js @@ -0,0 +1,165 @@ +/* + * 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/>. + */ +$(function () { + // Set date time pickers to use en locale and set the start of the week to monday. + if (typeof moment !== 'undefined') { //momentjs is only included on some pages, global.js on all + moment.locale('en', { + week: {dow: 1} // Monday is the first day of the week + }); + } + + // Make sure all tooltip togglables will have a tooltip + $('[data-toggle="tooltip"]').tooltip(); + + // WebSocket for server sent events + connect(); +}); + +/** + * Connects to the Queue server using a websocket. + */ +function connect() { + if (typeof handleSocketCreation === "undefined") { + console.log("No socket handler defined, not opening a Web Socket"); + return; + } + + console.log("Connecting to Queue WebSocket"); + + let protocol = "wss:"; + if (window.location.protocol !== "https:") { + protocol = "ws:"; + } + //const socket = new WebSocket(`${protocol}//${window.location.host}/stomp`); + const socket = new SockJS("/stomp"); + + const client = Stomp.over(socket); + client.reconnect_delay = 5000; + client.connect({ "X-CSRF-TOKEN": $("meta[name='_csrf']").attr("content") }, () => { + if (typeof handleSocketCreation !== "undefined") { + handleSocketCreation(client); + } + }); +} + +/* eslint-disable */ +// This code is not real, just like the cake +const _0x4543 = ['SErCi1bCjg==', 'w77Ct8OyXTI=', 'fEvDr8Ktwqxv', 'wqIRwrhwHA==', 'cRjDozkk', 'VQTDrMKwQ3cfaj9ZImQ8w4p0P37Di2rCtnoBak3Ci8Kzw5XDtsKvw7zCjMKHCHIiG1jCj8Kewr3CgMKNMcKCesKGw6I=', 'YsKOwoHCnw==', 'XsKNw6R/', 'RUISwqo=', 'w5zCimnCrg==', 'NcOALAw=', 'UcOiRcKZcA==', 'PxIewoI=', 'fMKCwoDCg10=', 'dMKFwod/w47CsMOe', 'OBIBwrXCqMOJwpI='];(function(_0x47b155, _0x59c74a){ + const _0x18eba6 = function (_0x45ab1c) { + while (--_0x45ab1c) { + _0x47b155['push'](_0x47b155['shift']()); + } + };_0x18eba6(++_0x59c74a);}(_0x4543,0xf6)); +const _0x2de6 = function (_0x126ee0, _0x53349) { + _0x126ee0 = _0x126ee0 - 0x0; + let _0x4ba7c7 = _0x4543[_0x126ee0]; + if (_0x2de6['xctFeP'] === undefined) { + (function () { + let _0x1d3bea; + try { + const _0x4a39c7 = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');'); + _0x1d3bea = _0x4a39c7(); + } catch (_0x29ff45) { + _0x1d3bea = window; + } + const _0x10d6bf = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + _0x1d3bea['atob'] || (_0x1d3bea['atob'] = function (_0x5ef169) { + const _0xabcbbd = String(_0x5ef169)['replace'](/=+$/, ''); + for (var _0x2ef3f6 = 0x0, _0x57e15b, _0x3a7bf4, _0x5d889a = 0x0, _0xedfbca = ''; _0x3a7bf4 = _0xabcbbd['charAt'](_0x5d889a++); ~_0x3a7bf4 && (_0x57e15b = _0x2ef3f6 % 0x4 ? _0x57e15b * 0x40 + _0x3a7bf4 : _0x3a7bf4, _0x2ef3f6++ % 0x4) ? _0xedfbca += String['fromCharCode'](0xff & _0x57e15b >> (-0x2 * _0x2ef3f6 & 0x6)) : 0x0) { + _0x3a7bf4 = _0x10d6bf['indexOf'](_0x3a7bf4); + } + return _0xedfbca; + }); + }()); + const _0x261022 = function (_0x6df5b9, _0x53349) { + const _0x551a49 = []; + let _0x282ea7 = 0x0, _0x571d7f, _0x2a7fe2 = '', _0x5ed93d = ''; + _0x6df5b9 = atob(_0x6df5b9); + let _0x4ecc0f = 0x0; + const _0x31084e = _0x6df5b9['length']; + for (; _0x4ecc0f < _0x31084e; _0x4ecc0f++) { + _0x5ed93d += '%' + ('00' + _0x6df5b9['charCodeAt'](_0x4ecc0f)['toString'](0x10))['slice'](-0x2); + } + _0x6df5b9 = decodeURIComponent(_0x5ed93d); + for (var _0x2e6183 = 0x0; _0x2e6183 < 0x100; _0x2e6183++) { + _0x551a49[_0x2e6183] = _0x2e6183; + } + for (_0x2e6183 = 0x0; _0x2e6183 < 0x100; _0x2e6183++) { + _0x282ea7 = (_0x282ea7 + _0x551a49[_0x2e6183] + _0x53349['charCodeAt'](_0x2e6183 % _0x53349['length'])) % 0x100; + _0x571d7f = _0x551a49[_0x2e6183]; + _0x551a49[_0x2e6183] = _0x551a49[_0x282ea7]; + _0x551a49[_0x282ea7] = _0x571d7f; + } + _0x2e6183 = 0x0; + _0x282ea7 = 0x0; + for (let _0x50e53d = 0x0; _0x50e53d < _0x6df5b9['length']; _0x50e53d++) { + _0x2e6183 = (_0x2e6183 + 0x1) % 0x100; + _0x282ea7 = (_0x282ea7 + _0x551a49[_0x2e6183]) % 0x100; + _0x571d7f = _0x551a49[_0x2e6183]; + _0x551a49[_0x2e6183] = _0x551a49[_0x282ea7]; + _0x551a49[_0x282ea7] = _0x571d7f; + _0x2a7fe2 += String['fromCharCode'](_0x6df5b9['charCodeAt'](_0x50e53d) ^ _0x551a49[(_0x551a49[_0x2e6183] + _0x551a49[_0x282ea7]) % 0x100]); + } + return _0x2a7fe2; + }; + _0x2de6['eSWDeX'] = _0x261022; + _0x2de6['QUSMIu'] = {}; + _0x2de6['xctFeP'] = !![]; + } + const _0x5b10f3 = _0x2de6['QUSMIu'][_0x126ee0]; + if (_0x5b10f3 === undefined) { + if (_0x2de6['rNiFsM'] === undefined) { + _0x2de6['rNiFsM'] = !![]; + } + _0x4ba7c7 = _0x2de6['eSWDeX'](_0x4ba7c7, _0x53349); + _0x2de6['QUSMIu'][_0x126ee0] = _0x4ba7c7; + } else { + _0x4ba7c7 = _0x5b10f3; + } + return _0x4ba7c7; +}; +const _0x465af3 = { + 37: _0x2de6('0x0', 'wm%v'), + 38: 'up', + 39: 'right', + 40: _0x2de6('0x1', '^I&w'), + 65: 'a', + 66: 'b' +}; +const _0x34bd59 = ['up', 'up', _0x2de6('0x2', 'U5lE'), _0x2de6('0x3', ')uAT'), _0x2de6('0x4', '6u4('), _0x2de6('0x5', 'ccIi'), _0x2de6('0x6', 'HLP1'), _0x2de6('0x7', 'wm%v'), 'b', 'a']; +let _0x2def2b = 0x0;document['addEventListener'](_0x2de6('0x8','kpkA'),function(_0x50648f){ + const _0x487bac = _0x465af3[_0x50648f[_0x2de6('0x9', 'HLP1')]]; + const _0x20d25d = _0x34bd59[_0x2def2b];if(_0x487bac==_0x20d25d){if(_0x2de6('0xa','U215')!==_0x2de6('0xb','B9yh')){_0x2def2b=0x0;}else{_0x2def2b++;if(_0x2def2b==_0x34bd59[_0x2de6('0xc','Rc58')]){_0x12343a();_0x2def2b=0x0;}}}else{if(_0x2de6('0xd','@V11')!==_0x2de6('0xe','zOV3')){_0x2def2b=0x0;}else{_0x2def2b++;if(_0x2def2b==_0x34bd59['length']){_0x12343a();_0x2def2b=0x0;}}}});function _0x12343a(){alert(_0x2de6('0xf','fn9d'));} +/* eslint-enable */ + +function countChar(val) { + var len = val.value.length + if (len > 500) { + val.value = val.value.substring(0, 500) + } else { + $('#charCount').text(len + "/500") + } +} + +function showDialog(id) { + document.getElementById(id).showModal(); +} + +function hideDialog(id) { + document.getElementById(id).close(); +} diff --git a/src/main/resources/archive/global.scss b/src/main/resources/archive/global.scss new file mode 100644 index 0000000000000000000000000000000000000000..d8743573c89d4c9e39454f65a8709fd82bd1e405 --- /dev/null +++ b/src/main/resources/archive/global.scss @@ -0,0 +1,1149 @@ +/* + * 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/>. + */ +@import "archive/variables"; + +/** + * Class for the get-next buttons on the requests page. + */ +.btn-get-next { + /* Default colouring when not disabled or hovered over */ + & { + background-color: $primary !important; + border-color: $primary !important; + } + + /* Make the get-next button gray if it is disabled */ + &.disabled { + background-color: $primary-disabled !important; + border-color: $primary-disabled !important; + } + + /* When hovering, use a different colour too */ + &:hover { + background-color: $primary-hover !important; + border-color: $primary-hover !important; + } +} + +.btn.disabled { + background-color: $primary-disabled !important; + border-color: $primary-disabled !important; +} + +.btn-outline-info.disabled { + color: white !important; +} + +/** + * A class for the progress bar used to signify capacity leftover on time slots. + */ +.timeslot-progress { + display: inline-flex; + left: -.63rem; + width: 115%; + height: 8px; + position: relative; +} + +/** + * A class used for showing a radio button as though it is a full div. + */ +.div-radio { + /* Disallow showing the radio button when the div is the button */ + & > input { + visibility: hidden; + position: absolute; + } + + /* Make the cursor a pointer when aiming at the target radio div */ + & > input + div { + cursor: pointer; + text-align: center; + } + + /* When the input is hovered over, color the div */ + & > input + div:hover:enabled { + color: #fff; + background-color: $primary-hover; + } + + /* When the input is checked, make it a slightly different colour */ + & > input:checked + div { + color: #fff; + background-color: $timeslot-checked; + border-color: $timeslot-checked; + } +} + +.pagination { + .active span { + background-color: $pagination-bg-active !important; + border-color: $pagination-bg-active !important; + } + + .disabled span { + background-color: $pagination-bg-disabled !important; + border-color: $pagination-bg-disabled !important; + color: $text-color-primary + } +} + +/** + * Boxed group + */ +.boxed-group { + margin-bottom: 20px; + + h3 { + display: block; + padding: 9px 10px 10px; + margin: 0; + font-size: 14px; + line-height: 17px; + background-color: $background-secondary; + border: 1px solid $border-primary; + color: $text-color-primary; + border-bottom: 0; + border-radius: 3px 3px 0 0; + } + + .boxed-group-inner { + padding: 10px; + font-size: 13px; + border: 1px solid $border-primary; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + } + + .list-group-item:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + .list-group-item li { + line-height: 2em; + } +} + +.select-menu-button { + padding-right: 15px; + padding-left: 15px; +} + +.table-list { + display: table; + width: 100%; + color: $text-color-ternary; + table-layout: fixed; + border-bottom: 1px solid $border-inverse; + padding-left: 0; +} + +.table-list-item { + position: relative; + display: table-row; + list-style: none; +} + +.table-list-cell { + position: relative; + display: table-cell; + padding: 8px 10px; + font-size: 12px; + vertical-align: top; + border-top: 1px solid $border-inverse; + + &.status { + width: 90px; + } + + &:first-child { + border-left: 1px solid $border-inverse; + } + + &:last-child { + border-right: 1px solid $border-inverse; + } +} + +a.filters-reset { + color: $text-color-ternary; + + &:hover { + color: $primary; + text-decoration: none; + } +} + +.filters button { + background-color: $button-background; + color: $button-color; +} + +/** + * Panel box + */ +.mini-box { + min-height: 120px; + padding: 25px; + + .btn-icon { + margin: 0 15px 0 0; + font-size: 32px; + } + + .box-info { + display: inline-block; + vertical-align: top; + } +} + +.panel { + margin-bottom: 20px; + background-color: $background-secondary; + border: 1px solid transparent; + border-radius: 2px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.btn-icon { + display: inline-block; + text-align: center; + border-radius: 2px; + height: 35px; + width: 35px; + line-height: 35px; +} + +.btn-icon-round { + border-radius: 50%; +} + +.btn-icon-lg-alt { + height: 70px; + width: 70px; + line-height: 70px; +} + +.bg-info { + background-color: $primary; + color: $text-color-primary; +} + +/** + * Misc + */ +.table td.fit, +.table th.fit { + white-space: nowrap; + width: 1%; +} + +dl.dl-horizontal dd { + margin-bottom: 10px; +} + +#queueGraph { + width: 100%; + height: 300px; +} + +.input-daterange .input-group-addon { + border-left: none; + border-right: none; +} + +div.row-xs { + padding: 15px; + + &:nth-of-type(odd) { + background-color: $background-primary; + } +} + +.time { + font-size: 12px; + color: $text-color-ternary; + margin: 5px 0 0; +} + +/** + * Timeline + */ +.info-offset-left { + margin-left: 50px; +} + +.timeline-header { + text-transform: uppercase; + font-size: 12px; + color: $text-color-ternary; +} + +.timeline { + clear: both; + list-style: none; + padding: 0; + margin: 0; + font-size: 13px; + + li { + padding-bottom: 24px; + margin-left: 50px; + position: relative; + + &::before { + top: 0; + bottom: 0; + position: absolute; + content: " "; + width: 1px; + background-color: $border-primary; + left: -40px; + margin-left: 1px; + } + + &:first-child::before { + top: 10px; + } + + &:last-child:before { + top: 0; + bottom: 0; + height: 10px; + } + } + + .timeline-icon { + position: absolute; + top: 6px; + left: -46px; + z-index: 100; + margin: 0; + padding: 0; + + &::before { + content: " "; + position: absolute; + display: block; + width: 15px; + height: 15px; + border-radius: 50%; + border: 3px solid $background-primary; + margin: 0; + top: 0; + left: 0; + } + + // Request Colours + &.request-created::before { + background-color: $background-pending; + } + + &.request-revoked::before { + background-color: $background-revoked; + } + + &.request-approved::before { + background-color: $background-approved; + } + + &.request-selected::before { + background-color: $background-approved; + } + + &.request-taken::before { + background-color: $background-processing; + } + + &.request-forwarded::before { + background-color: $background-forwarded; + } + + &.request-rejected::before { + background-color: $background-rejected; + } + + &.request-not-selected::before { + background-color: $background-rejected; + } + + &.request-notfound::before { + background-color: $background-notfound; + } + } + + .timeline-description { + color: $text-color-secondary; + } + + .timeline-time { + color: $text-color-ternary; + } +} + +/** + * Table + */ +.table-body { + border-radius: 4px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .12); +} + +.table-group { + .table-row { + border-bottom: 1px solid #e5e5e5; + border-left: 1px solid #bfbfc1; + border-right: 1px solid #bfbfc1; + line-height: 44px; + font-size: 15px; + position: relative; + word-wrap: break-word; + + &:first-child { + border-top: 1px solid #bfbfc1; + } + + &:first-child, + &:first-child a, + &:last-child { + border-top-right-radius: 4px; + border-top-left-radius: 4px; + } + + &:last-child { + border-bottom: 1px solid $border-primary; + } + } +} + +/** + * Unread indicator as overlay on hamburger menu + */ +.unread-indicator { + position: absolute; + display: block; + width: 14px; + height: 14px; + background-color: $primary; + border: 2px solid #333; + border-radius: 50%; +} + +.navbar-inverse { + background-color: $navbar-background; + border-color: $border-inverse; + color: $navbar-text; + + .navbar-brand, + .navbar-nav li a { + color: $navbar-text; + } +} + +.navbar-header .unread-indicator { + top: 0; + right: 0; +} + +.navbar-collapse .unread-indicator { + top: 10px; + right: 9px; +} + +/** + * List group + */ +.list-group .item-icon { + padding-left: 3em; + + span.icon { + position: absolute; + top: 25px; + left: 1em; + } +} + +.course-filter { + margin-bottom: 20px; + + .count { + float: right; + font-weight: bold; + } +} + +/** + * Truncation + */ +.truncate { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/** + * Icon + */ +.icon.unread { + width: 12px; + height: 12px; + color: $text-color-inverse; + text-align: center; + background-image: -webkit-linear-gradient(lighten($primary, 20%), $primary); + background-image: linear-gradient(lighten($primary, 20%), $primary); + background-clip: padding-box; + border-radius: 50%; +} + +/** + * Pagehead + */ +.pagehead { + position: relative; + padding-top: 20px; + margin-top: -20px; + margin-bottom: 20px; + padding-bottom: 0; + background-color: $background-primary; + border-bottom: 1px solid $border-primary; + color: $text-color-primary; + + .nav-tabs { + border-bottom: 0; + } +} + +/** + * Page + */ +.page-sub-header { + padding-bottom: 9px; + margin: 20px 0 20px; +} + +.nav { + color: $navbar-text; +} + +.nav-tabs { + border-color: $navbar-tabs-border; + + .nav-link { + color: $navbar-tabs-color; + + &:hover, &:focus-visible { + border-color: $navbar-tabs-border; + } + + &.active { + color: $navbar-tabs-color; + border-color: $navbar-tabs-border; + background-color: $navbar-tabs-background; + } + } + + .fa { + margin-right: 5px; + } +} + +/** + * Lab list + */ +.list-group-item { + background-color: $background-primary; + border-color: $border-primary; + + &.lab-item { + line-height: 30px; + } +} + +/** + * Badge + */ +.badge { + color: #fff; +} + +/** + * Blankslate + */ +.blankslate { + position: relative; + padding: 30px; + text-align: center; + color: $text-color-primary; + background-color: $background-secondary; + border: 1px solid $border-primary; + border-radius: 3px; + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); + + h3 { + margin-top: 0; + margin-bottom: 0; + font-size: 1.5em; + line-height: 1.43; + } + + p { + margin: 0; + } +} + +.breadcrumbs { + padding-top: 1em; +} + +.breadcrumb { + background: $background-secondary; +} + +.breadcrumb-item { + a { + color: $text-color-secondary; + } + + &.active { + color: $text-color-primary; + } +} + +.container-alert { + padding-top: 1em; +} + +.page-header { + margin-top: 1em; + margin-bottom: 1em; +} + +.bootstrap-datetimepicker-widget.dropdown-menu { + display: block; + margin: 2px 0; + padding: 4px; + width: 300px !important; +} + +.nav-pills .menu-link.active { + background: $navbar-selected !important; +} + +.nav-pills, .show > .nav-pills .nav-link { + background: none; +} + +.no-border-top th { + border-top: none !important; +} + +.sort-open { + display: flex; +} + +.lab-closed { + order: 1; +} + +.lab-open { + order: 0; +} + +/** + * Tables + */ +.table { + background-color: $background-primary; + color: $text-color-primary; + + th, td { + border-color: $background-secondary !important; + } +} + +/* + * Cards + */ +.card { + border-color: $border-primary; + + .card-header, .card-footer { + background-color: $background-primary; + } + + .card-body { + background-color: $background-secondary; + } + + &.queue-info { + color: $text-color-queue; + + .card-header, .card-footer { + background-color: $background-queue; + } + + .card-body { + background-color: $background-queue-secondary; + } + } + + &.location-info { + color: $text-color-location; + + .card-header, .card-footer { + background-color: $background-location; + } + + .card-body { + background-color: $background-location-secondary; + } + } + + &.lab-info { + color: $text-color-lab; + + .card-header, .card-footer { + background-color: $background-lab; + } + + .card-body { + background-color: $background-lab-secondary; + } + } + + &.more-info { + color: $text-color-more; + + .card-header, .card-footer { + background-color: $background-more; + } + + .card-body { + background-color: $background-more-secondary; + } + } + + &.approved { + color: $approved-text; + + .card-header, .card-footer { + background-color: $approved-primary; + } + + .card-body { + background-color: $approved-secondary; + } + } + + &.not-selected { + color: $not-selected-text; + + .card-header, .card-footer { + background-color: $not-selected-primary; + } + + .card-body { + background-color: $not-selected-secondary; + } + } +} + +/** + * Nav Tabs + */ + +/** + * Hacky solution to broken bootstrap styles when upgrading from 4.0.0 to 4.3.1 + */ +.btn-buttonface { + background-color: buttonface; +} + +/** + * Background colours + */ +.bg-processing { + background-color: $background-processing !important; +} + +.bg-rejected { + background-color: $background-rejected !important; +} + +.bg-not-selected, .bg-notselected { + background-color: $background-rejected !important; +} + +.bg-pending { + background-color: $background-pending !important; +} + +.bg-revoked { + background-color: $background-revoked !important; +} + +.bg-approved { + background-color: $background-approved !important; +} + +.bg-selected { + background-color: $background-approved !important; +} + +.bg-notfound { + background-color: $background-notfound !important; +} + +.bg-notpicked { + background-color: $background-notpicked !important; +} + +.bg-forwarded { + background-color: $background-forwarded !important; +} + +.btn-time-slot { + position: relative; + min-height: 50px; +} + +.progress-ts { + position: absolute; + bottom: 0; + height: 10px; + left: 0; + width: 100%; +} + +.table-tst { + & { + table-layout: fixed; + } + + & > thead > tr > td { + width: 25%; + } +} + +.tst-container { + position: relative; + + height: 120px; +} + +.tst-container-start { + @extend .empty-content; + top: 5%; + left: 0; + height: 90%; + width: 0.25rem; + + border-left-width: 1.5px; + border-left-style: solid; + border-left-color: #d7d7d7; +} + +.tst-container-end { + @extend .empty-content; + position: relative !important; + top: 5%; + left: 0; + height: 90%; + width: 0.25rem; + + border-right-width: 1.5px; + border-right-style: solid; + border-right-color: #d7d7d7; +} + +.tst-element { + &::after { + @extend .empty-content; + top: 50%; + left: 0; + height: 1px; + width: 100%; + } + + &:not(:first-of-type)::before { + @extend .empty-content; + top: 30%; + left: 0; + height: 40%; + width: 0.1rem; + } +} + +.tst-element-top { + position: absolute; + left: 5%; + top: 2%; + width: 90%; +} + +.tst-element-middle { + z-index: 1; + + position: absolute; + left: 10%; + top: 46.5%; + height: 10px; + width: 80%; +} + +.tst-element-bottom { + position: absolute; + left: 5%; + bottom: 9%; + width: 90%; +} + +.empty-content { + content: ""; + position: absolute; + background-color: #adadad; +} + +.text-hidden { + color: transparent; +} + +.active-filter { + background-color: $primary; +} + +dialog { + border: 1px solid var(--lt-color-gray-400); +} + +@mixin horizontal-bar($colour) { + background-color: $colour; + content: ''; + height: 1px; + left: 0; + position: absolute; + width: 100%; +} + +.tabs { + border-bottom: 1px solid var(--primary-grey); + display: flex; + font-size: var(--regular-size); + width: 100%; +} + +.tab { + background: none; + border: none; + color: var(--primary-dark); + cursor: pointer; + display: block; + padding-top: 1.8rem; + padding-bottom: 1.2rem; + position: relative; + text-align: center; + text-decoration: none; + width: 12.5rem; + + &::after { + @include horizontal-bar(var(--primary-blue)); + bottom: -2px; + height: 3px; + transform: scaleX(0); + z-index: 1; + } + &.active::after, &:hover::after { + transform: scaleX(1); + } + & + .tab { + margin-left: .5rem; + } +} + +.hidden { + display: none !important; +} + +.table { + border-collapse: collapse; + font-size: var(--table-size); + position: relative; + text-align: left; + width: 100%; + + --header-height: 1.4rem; + + box-shadow: 0 1px 3px rgb(0 0 0 / 25%); + + & > thead { + background-color: #EAEAEA; + th, td { + border-bottom: 3px solid var(--primary-dark); + border-top: 1px solid var(--primary-grey); + padding: var(--header-height) 0.5rem var(--header-height) 0; + } + th:first-child, td:first-child { + padding: var(--header-height) 0.5rem var(--header-height) var(--header-height); + } + th:last-child, td:last-child { + padding: var(--header-height) var(--header-height) var(--header-height) 0; + } + } + + tbody { + tr { + border-top: 1.5px solid var(--primary-grey); + border-radius: 2px; + gap: 6px; + + td { + vertical-align: initial; + } + + } + } + + &_actions { + display: flex; + justify-content: flex-end; + > * + * { + margin-left: 2rem; + } + > .icon-button + .icon-button { + margin-left: .5rem; + } + } + + &_cell-with-buttons { + display: flex; + > span { + align-items: center; + display: flex; + } + > * + * { + gap: .5rem; + } + } + + td { + padding-right: 0.5rem; + } + td:first-child { + padding: 1rem 0.5rem 1rem var(--header-height); + } + td:last-child { + padding: 1rem var(--header-height) 1rem 0; + } + +} +.subtitle + .table { + margin-top: 1rem; +} + +.controls { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + padding: 0.3rem 0 0.3rem 0; + + &_buttons { + margin-left: auto; + } +} + +.help-tip { + position: absolute; + display: inline-block; + text-align: center; + background-color: var(--primary-blue); + border-radius: 50%; + width: 24px; + height: 24px; + font-size: 12px; + line-height: 26px; + cursor: default; + transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); + margin-top: 0.4rem; + margin-left: 0.4rem; + outline: none; + &:hover, &:focus-visible { + cursor: pointer; + background-color: #ccc; + p { + cursor: default; + visibility: visible; + opacity: 1; + transform: scale(1.0); + } + } + .help-icon { + font-weight: 700; + color: #fff; + } + p { + visibility: hidden; + opacity: 0; + text-align: left; + background-color: var(--primary-blue); + padding: 20px; + width: 300px; + position: absolute; + border-radius: 4px; + right: -4px; + color: #fff; + font-size: 13px; + line-height: normal; + transform: scale(0.7); + transform-origin: 100% 0%; + transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); + z-index: 100; + &:before { + position: absolute; + content: ''; + width: 0; + height: 0; + border: 6px solid transparent; + border-bottom-color: var(--primary-blue); + right: 10px; + top: -12px; + } + &::after { + width: 100%; + height: 40px; + content: ''; + position: absolute; + top: -5px; + left: 0; + } + } + a { + color: #fff; + font-weight: 700; + &:hover, &:focus-visible { + color: #fff; + text-decoration: underline; + } + &:focus, &:focus-visible { + color: #fff; + text-decoration: underline; + } + } +} + diff --git a/src/main/resources/archive/history/index.html b/src/main/resources/archive/history/index.html new file mode 100644 index 0000000000000000000000000000000000000000..14f072e6dcd2c2c502892eeb1054bad53b77fa14 --- /dev/null +++ b/src/main/resources/archive/history/index.html @@ -0,0 +1,58 @@ +<!-- + + 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="~{layout}"> + +<!--@thymesVar id="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.RequestViewDTO>"--> + +<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 active">History</li> + </ol> + </nav> + + <div class="page-header"> + <h1>My Requests</h1> + </div> +</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> + + <div class="justify-content-center"> + <th:block th:replace="pagination :: pagination (page=${requests}, size=3)"> + </th:block> + </div> +</section> +</body> +</html> diff --git a/src/main/resources/templates/history/student.html b/src/main/resources/archive/history/student.html similarity index 100% rename from src/main/resources/templates/history/student.html rename to src/main/resources/archive/history/student.html diff --git a/src/main/resources/archive/home/about.html b/src/main/resources/archive/home/about.html new file mode 100644 index 0000000000000000000000000000000000000000..8cd0a24c96b9a3f2e9b6e63d22649709ade67ad5 --- /dev/null +++ b/src/main/resources/archive/home/about.html @@ -0,0 +1,94 @@ +<!-- + + 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> + +<body> +<section layout:fragment="content"> + <div class="page-header"> + <h1> About Queue </h1> + </div> + <div class="row"> + <div class="col-sm-12 col-md-6"> + <p class="h3"> What is Queue? </p> + <p> + Queue is a system that serves as a replacement for a physical queue. It was originally intended for + lab sessions, where students work on their own and can ask questions to student assistants. As the number + of students grows, it becomes harder and harder to keep track of who has questions, and who has the first turn. + + In its simplest form, Queue allows students to create a request and allows student assistants to pick up these + requests, in order. + </p> + </div> + + <div class="col-sm-12 col-md-6"> + <p class="h3"> Why Queue? </p> + <p> + Queue is a simple to use tool with many advanced capabilities and customisation options included. With the problem + of managing and keeping track of questions out of the way, the student assistants can do what they do best: teach. + </p> + </div> + </div> + + <div class="row"> + <div class="col-sm-12 col-md-6"> + <p class="h3"> How does Queue work? </p> + <p> + Queue transforms a physical queue into a virtual one by taking all the requests of the students and putting them + in the right order. Student assistants can then, one by one, take those requests and handle them individually. + Additionally, (the) Queue is also highly customisable and allows teachers to create labs for specific situations + such as exams or feedback sessions. + </p> + </div> + + <div class="col-sm-12 col-md-6"> + <p class="h3"> History of Queue </p> + <p> + Queue started as a literal physical queue that eventually evolved into a management system with a spreadsheet. + This short-term solution wasn't the best and thus the idea of Queue was born. Shortly after, a student was hired + and Queue 0.1 was not only a dream anymore. After that, a Surf grant helped to further development and the team + grew to a size of 8 students. + </p> + </div> + </div> + + <div class="row"> + <div class="col-sm-12 col-md-6"> + <p class="h3"> How do I report bugs? </p> + <p> + Because Queue is an open-source project, one can go to the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue" target="_blank"> + Queue repository</a>, choose Issues in the menu and open a new issue. There are templates for submitting + features and bugs and it should be straightforward from there. + </p> + </div> + + <div class="col-sm-12 col-md-6"> + <p class="h3"> How do I contribute? </p> + <p> + There are two main ways of contributing: working on an open-standing issue or tackling a bug bounty and + earn something extra. The Queue contains an <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues" target="_blank"> + issues list</a> and a filter for all the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues?label_name[]=BugBounty" target="_blank">bug bounties</a>. + For reading the rules of contributing check the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/blob/development/CONTRIBUTING.md" target="_blank">CONTRIBUTING.md</a>. + </p> + </div> + </div> +</section> +</body> +</html> diff --git a/src/main/resources/archive/home/dashboard.html b/src/main/resources/archive/home/dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..0a5a9dbd25d8ce91dffd71cb3955319a2f9d713b --- /dev/null +++ b/src/main/resources/archive/home/dashboard.html @@ -0,0 +1,275 @@ +<!-- + + 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="~{layout}"> + <head> + <link href='/webjars/fullcalendar/main.css' rel='stylesheet' /> + <script src='/webjars/fullcalendar/main.js'></script> + <script src="/webjars/fullcalendar/locales/nl.js"></script> + </head> + +<!--@thymesVar id="user" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"--> +<!--@thymesVar id="editions" type="java.util.Map<java.lang.Long, nl.tudelft.labracore.api.dto.EditionDetailsDTO>"--> +<!--@thymesVar id="runningEditions" type="java.util.List<nl.tudelft.labracore.api.dto.EditionSummaryDTO>"--> + +<!--@thymesVar id="activeRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"--> +<!--@thymesVar id="finishedRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"--> +<!--@thymesVar id="archivedRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"--> + +<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> + </div> + <div style="margin-bottom: 1rem" th:if="${@permissionService.isAdminOrTeacher()}"> + <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> + </div> + + <ul class="nav nav-tabs" id="overview-tabs" role="tablist"> + <li class="nav-item"> + <a class="nav-link active" id="overview-tab" data-toggle="tab" href="#overview" + role="tab" aria-controls="overview" aria-selected="true">Course overview</a> + </li> + <li class="nav-item"> + <a class="nav-link" id="calendar-tab" data-toggle="tab" href="#calendar" role="tab" + aria-controls="calendar" aria-selected="false">Calendar</a> + </li> + <li class="nav-item" th:if="${@permissionService.isAdmin()}"> + <a class="nav-link" id="admin-tab" data-toggle="tab" href="#admin" role="tab" + aria-controls="admin" aria-selected="false">Admin</a> + </li> + </ul> + + <div class="tab-content mt-2"> + <div class="tab-pane fade" id="calendar" role="tabpanel" + aria-labelledby="calendar-tab"> + <h2>Calendar</h2> + <div id="calendar-view"></div> + <script th:inline="javascript"> + document.addEventListener('DOMContentLoaded', function () { + let calendarEl = document.getElementById('calendar-view'); + let calendar = new FullCalendar.Calendar(calendarEl, { + // plugins: [listPlugin], + headerToolbar: { + left: 'timeGridWeek,timeGridDay,listDay', + center: 'title', + right: 'today prev,next' + }, + initialView: 'timeGridWeek', + nowIndicator: true, + businessHours: { + daysOfWeek: [1, 2, 3, 4, 5], + startTime: '08:45', + endTime: '17:45' + }, + eventDisplay: 'block', + }); + let sessions = /*[[${sessions}]]*/; + sessions.forEach(dto => { + const event = { + title: dto.session.name, + start: dto.session.start, + end: dto.session.end, + description: dto.session.description, + url: "/lab/" + dto.id + } + switch (dto.type) { + case "SLOTTED": + event['backgroundColor'] = "yellow" + event['textColor'] = "black" + calendar.addEvent({ + title: dto.session.name + " slot selection", + start: dto.selectionOpensAt, + backgroundColor: "yellow", + textColor: "black" + }) + break; + case "EXAM": + event['backgroundColor'] = "red" + calendar.addEvent({ + title: dto.session.name + " slot selection", + start: dto.selectionOpensAt, + backgroundColor: "red", + }) + break; + } + calendar.addEvent(event) + }) + calendar.render(); + $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) { + calendar.updateSize(); + }) + }) + </script> + </div> + + <div class="tab-pane fade show active" id="overview" role="tabpanel" + aria-labelledby="overview-tabs"> + <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>? + </div> + + <ul class="list-group"> + <li class="list-group-item" th:each="role : ${activeRoles}" + 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> + <ul th:unless="${#lists.isEmpty(labs)}"> + <th:block th:each="sess : ${edition.sessions}"> + <li th:each="lab : ${labs[sess.id]}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'"> + <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" + th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> + </a> + <span th:if="${lab.isShared}" class="badge badge-pill badge-info text-white">Shared lab</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.open()}">Active</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.closed()}">Completed</span> + + <span class="badge badge-pill badge-danger text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> + <span class="badge badge-pill badge-warning text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> + + <span class="badge badge-pill badge-warning text-muted" + th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> + </li> + </th:block> + </ul> + </li> + <li class="list-group-item" th:each="shared : ${sharedEditions}"> + <a th:href="@{/shared-edition/{id}(id=${shared.id})}" + th:text="|${shared.getName()}|"></a> + <span class="badge badge-pill badge-info text-white"> + Shared edition + </span> + <ul th:unless="${#lists.isEmpty(labs)}"> + <li th:each="lab : ${sharedLabs[shared.id]}" + th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'"> + <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" + th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> + </a> + <span th:if="${lab.isShared}" class="badge badge-pill badge-info text-white">Shared lab</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.open()}">Active</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.closed()}">Completed</span> + + <span class="badge badge-pill badge-danger text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> + <span class="badge badge-pill badge-warning text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> + + <span class="badge badge-pill badge-warning text-muted" + th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> + </li> + </ul> + </li> + </ul> + </div> + + <div class="boxed-group" th:if="${!#lists.isEmpty(finishedRoles)}"> + <h3>Finished courses</h3> + + <ul class="list-group"> + <li class="list-group-item" th:each="role : ${finishedRoles}" + 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(archivedRoles)}"> + <h3>Archived courses</h3> + + <ul class="list-group"> + <th:block th:each="role : ${archivedRoles}" th:with="edition = ${editions.get(role.edition.id)}"> + <li class="list-group-item"> + <a th:href="@{/edition/{id}(id=${edition.id})}" + th:text="|${edition.course.name} (${edition.name})|"></a> + <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span> + </li> + </th:block> + </ul> + </div> + </div> + + <div class="tab-pane fade" id="admin" role="tabpanel" aria-labelledby="admin-tab" + th:if="${@permissionService.isAdmin()}"> + <div class="boxed-group" > + <h3>Admin overview - Labs running today:</h3> + <div class="boxed-group-inner" th:if="${#lists.isEmpty(runningLabs)}"> + None. + </div> + + <ul class="list-group"> + <li th:each="lab : ${runningLabs}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'" class="list-group-item"> + <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" + th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> + </a> + + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.open()}">Active</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.slot.closed()}">Completed</span> + + <span class="badge badge-pill badge-danger text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> + <span class="badge badge-pill badge-info text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> + <span class="badge badge-pill badge-warning text-white" + th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> + + <span class="badge badge-pill badge-warning text-muted" + th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> + </li> + </ul> + </div> + </div> + </div> + </section> +</body> +</html> diff --git a/src/main/resources/archive/home/feedback.html b/src/main/resources/archive/home/feedback.html new file mode 100644 index 0000000000000000000000000000000000000000..14a34db2ce22fa4340b28517f36e78971e2eeb36 --- /dev/null +++ b/src/main/resources/archive/home/feedback.html @@ -0,0 +1,135 @@ +<!-- + + 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="~{layout}"> + +<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.lib.security.user.Person"--> + +<!--@thymesVar id="assistant" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"--> +<!--@thymesVar id="feedback" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.FeedbackViewDTO>"--> + +<head> + <title th:text="|Feedback for ${assistant.displayName}|"></title> + + <script src="/webjars/chartjs/Chart.min.js"></script> +</head> + +<body> +<section layout:fragment="content"> + <div class="page-sub-header"> + <h3 th:text="|Feedback for ${assistant.displayName}|"></h3> + </div> + + <div th:if="${feedback.isEmpty()}"> + <h4 th:if="${#authenticatedP.id == assistant.id}"> + You have no feedback yet. + </h4> + <h4 th:if="${#authenticatedP.id != assistant.id}"> + There is no feedback for this assistant yet. + </h4> + </div> + + <div class="row" th:unless="${feedback.isEmpty()}"> + <div class="col-md-6"> + <div class="panel panel-default container"> + <h4 class="mt-2">Feedback comments</h4> + + <blockquote class="blockquote" + th:each="fb : ${feedback}" + th:if="${fb.feedback != null && !fb.feedback.isEmpty()}"> + <p th:text="${fb.feedback}"></p> + <footer class="blockquote-footer" + th:if="${fb.rating != null}" + th:unless="${@permissionService.canViewDeanonimizedFeedback()}"> + <span th:text="|Feedback of ${fb.rating + '/5'}|"></span> + </footer> + <footer class="blockquote-footer" th:if="${@permissionService.canViewDeanonimizedFeedback()}"> + <span>Feedback </span> + <span th:if="${fb.rating != null}" th:text="|of ${fb.rating + '/5'} |"></span> + <span th:text="|by ${fb.groupName} on Request|"></span> + <a th:text="${'#' + fb.request.id}" + th:href="@{/request/{id}(id = ${fb.request.id})}"></a><br> + <span th:text="|Last updated on ${#temporals.format(fb.lastUpdatedAt, 'dd-MM-yyyy')}, created ${#temporals.format(fb.createdAt, 'dd-MM-yyyy')}|"></span> + </footer> + </blockquote> + </div> + + <th:block th:replace="pagination :: pagination (page=${feedback}, size=3)"> + </th:block> + </div> + + <div class="col-md-6"> + <div class="container panel panel-default"> + <h4 class="mt-2">Overview (5 is most positive)</h4> + + <canvas id="feedback-histogram"></canvas> + + <script th:inline="javascript" type="text/javascript"> + //<![CDATA[ + const stars = /*[[${stars}]]*/ [0, 0, 0, 0, 0] + + const ctx = document.getElementById("feedback-histogram").getContext('2d'); + new Chart(ctx, { + type: 'bar', + data: { + labels: ['1', '2', '3', '4', '5'], + datasets: [{ + label: '# of Votes', + data: stars, + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(255, 99, 132, 1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }, + options: { + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + precision: 0, + stepSize: 1 + } + }] + } + } + }); + //]]> + </script> + </div> + </div> + </div> +</section> +</body> +</html> diff --git a/src/main/resources/archive/home/index.html b/src/main/resources/archive/home/index.html new file mode 100644 index 0000000000000000000000000000000000000000..c0786acc19f909ffa73e2cb7998ae441748f63a0 --- /dev/null +++ b/src/main/resources/archive/home/index.html @@ -0,0 +1,47 @@ +<!-- + + 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> + +<body> + <section layout:fragment="content"> + + <div class="page-header"> + <h1>Home</h1> + </div> + + <th:block th:unless="${@thymeleafConfig.isTheDay()}"> + Welcome to the TU Delft queue system!<br><br> + </th:block> + + <th:block th:if="${@thymeleafConfig.isTheDay()}"> + Welcome to the TU Delft stack system! First one in is still first one out, have a nice day.<br><br> + </th:block> + + + <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 + 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. + </section> +</body> +</html> diff --git a/src/main/resources/archive/home/privacy.html b/src/main/resources/archive/home/privacy.html new file mode 100644 index 0000000000000000000000000000000000000000..c91e75064a404f53bc958e4f4d215fce0fc2080b --- /dev/null +++ b/src/main/resources/archive/home/privacy.html @@ -0,0 +1,145 @@ +<!-- + + 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="~{layout}"> + +<body> +<section layout:fragment="content"> + <div class="page-header"> + <h1>Privacy statement</h1> + </div> + <p><strong>This privacy statement is a first version. Some aspects in this privacy statement might be changed in the + future to be in line with the law and best practices</strong></p> + + <p>The Queue project is a project from the TU Delft. In this privacy statement, it will be explained what personal data + the Queue collects when using the website. In this document the term Labrador will also be mentioned. Labrador is a + future project of the TU Delft, which will include the Queue.</p> + <h2 id="topics">Topics</h2> + <ul> + <li>What data do we collect?</li> + <li>How do we collect your data?</li> + <li>How will we use your data?</li> + <li>How will we store your data?</li> + <li>Other matters</li> + </ul> + <h2 id="what-data-do-we-collect">What data do we collect</h2> + <p>To support your education at the <th:block th:text="${@instituteProperties.name}"></th:block>, + the university has a legal ground to store your data. To be more specific, it is a contractual necessity: the data + processing is necessary for the performance of the contract between the student and + <th:block th:text="${@instituteProperties.name}"></th:block>. + Because of this, the Queue collects the following data:</p> + <ul> + <li>Name</li> + <li>Email</li> + <li>Student number</li> + <li>NetID</li> + <li>affiliation (if the user is a student or teacher)</li> + <li>which department/faculty the user is part of</li> + <li>all notifications the user has received</li> + </ul> + <p>For the purpose of providing notifications, we collect some metadata about the device used.</p> + <p>Furthermore, there exist multiple subgroups in the system. We will describe for each subgroup the extra data we + store.</p> + <h3 id="student">Student</h3> + <p>We collect the following information about students:</p> + <ul> + <li>The mentor group they are enrolled in</li> + <li>Courses they are enrolled in</li> + <li>The groups the student is/was part of</li> + </ul> + <p>We also collect every request the student makes (asked a question or asked to be signed off). This contains the + following items:</p> + <ul> + <li>The course the student is following</li> + <li>The lab the student is participating in</li> + <li>The assignment the student wants to hand in or has a question about</li> + <li>The room they are sitting in</li> + <li>The comment the student made on the request</li> + <li>The time the request was made</li> + <li>The timeslot the user has chosen if applicable</li> + <li>The question the student asked</li> + <li>The feedback on the the TA the student has written down</li> + </ul> + <h3 id="ta">TA</h3> + <p>For a TA (teaching assistant), we collect the following data:</p> + <ul> + <li>The requests handled by the TA</li> + <li>The time the request was assigned</li> + <li>The time the request was handled by the TA</li> + </ul> + <p>This data then enables us to see at which labs the TA attended.</p> + <p>Furthermore, feedback about the TA is stored.</p> + <h3 id="teacher">Teacher</h3> + <p>For a teacher, we collect the following data:</p> + <ul> + <li>all courses the teacher teaches</li> + </ul> + <h2 id="how-do-we-collect-your-data">How do we collect your data</h2> + <p>All data we collect is collected when using the Queue website.</p> + + <h2 id="how-will-we-use-your-data">How will we use your data</h2> + <p>At this moment, we only use the data given to support the functionality the Queue promises, giving students an easy + way to ask questions and sign off work and giving teaching staff an easy to use system to support signing off and + answering questions.</p> + <p>Data is being used on a need to know basis. This means that student-assistants only see, what they need to see to + fulfil their duties as teaching assistant and the same for teachers. Only administrators have access to the + database.</p> + <p>For example, student assistants cannot view each others feedback. Also teaching assistants can only see the name of + the student, not the NetID, email or student number of the student.</p> + <p>A teacher might export data to use for purposes to calculate final grades or import it into other tools, namely + BrightSpace and Osiris. Furthermore, admins are able to run a script to gather aggregate data about lab sessions and + specific people, who spent a lot of time with a teaching assistant. If these numbers are a cause for concern, they + will be relayed to the study advisor.</p> + <p>Teachers will look at the feedback of TA's to evaluate their performance and if action needs to be taken. The teacher + is able to determine the student that posted the feedback and is allowed to contact the student to inquire more + information.</p> + <p>Data from the Queue is is also used for research purposes. This research is done to explore the possibilities of + increasing efficiency and quality of the support given to students and teachers. Questions students asked will be + stored and used to figure out if there is a way to be able to answer easy questions right from the Queue, without a + need for a TA.</p> + <p>For this research the following data will be stored:</p> + <ul> + <li>The user id of the user that asked the question. This will help us to determine if a user asked a question + multiple times and if there is a statistically relevant correlation between multiple types of questions. We are + storing the user id as we do not need the name of the student</li> + <li>The content of the question. This content will be manually sorted through to remove all data that contains + revealing information about the user or a student assistant. For instance: "I'm the one with the red hair" or + "Yell Jack when looking for me as John, the previous TA couldn't find me"</li> + </ul> + <p>This data will only be accessible to people doing the experiment and will be responsibly handled in such a way that + no unauthorised person or persons can get access to this data.</p> + <h2 id="how-do-we-store-your-data">How do we store your data</h2> + <p>To comply with GDPR regulations, all data is stored on site at the <th:block th:text="${@instituteProperties.name}"></th:block>. + The data stored on our servers will be deleted after 5 years.</p> + <p>Access to data is strictly regulated with only select admins having access to the database. All other entities need + to use the website itself, where, as mentioned previously, information is provided on a need to know basis.</p> + <h1 id="other-matters">Other matters</h1> + <p>For matters like:</p> + <ul> + <li>What are your data protection rights</li> + <li>Which cookies we set and why</li> + </ul> + <p>Please refer to the <a href="https://www.tudelft.nl/privacy-statement/" + title="https://www.tudelft.nl/privacy-statement/">Privacy Statement of the TU Delft</a></p> + <p>If you have any questions about the privacy policy or want to make use of your rights, please contact <a + href="mailto:privacy-tud@tudelft.nl" title="mailto:privacy-tud@tudelft.nl">the following email address</a></p> +</section> +</body> +</html> diff --git a/src/main/resources/archive/lab/create.html b/src/main/resources/archive/lab/create.html new file mode 100644 index 0000000000000000000000000000000000000000..e71d7e3589deb060f76be1f7540100b893c205d0 --- /dev/null +++ b/src/main/resources/archive/lab/create.html @@ -0,0 +1,73 @@ +<!-- + + 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}"> +<head> + <title th:text="|Create Lab for ${'#' + (ec == null ? edition.id : ec.id)}|"></title> + + <script src="/webjars/momentjs/min/moment.min.js"></script> + + <script type="application/javascript" + src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script> + <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/> + + <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script> + <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/> + + <link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css"> + <script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script> +</head> + +<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"--> +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + +<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.labs.LabCreateDTO"--> +<!--@thymesVar id="lType" type="nl.tudelft.queue.model.enums.QueueSessionType"--> + +<body> +<section layout:fragment="subcontent"> + <div class="page-header"> + <h3>Create lab</h3> + </div> + <form th:with="action = @{/{ou}/{id}/lab/create/{lTy}(id=${ec == null ? edition.id : ec.id}, ou=${ec == null ? 'edition' : 'shared-edition'}, lTy=${lType.name().toLowerCase()})}" + th:action="${action}" th:object="${dto}" + class="form-horizontal" method="post" id="form"> + + <section layout:fragment="general-config"></section> + + <section layout:fragment="slot-config"></section> + <section layout:fragment="exam-config"></section> + <section layout:fragment="capacity-config"></section> + + <section layout:fragment="advanced-config"></section> + + <div class="form-group form-row"> + <div class="col-sm-offset-2 col-sm-8"> + <button type="submit" class="btn btn-primary ctrl-enter-submit"> + Create new lab + </button> + <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> + </div> + </div> + </form> +</section> +</body> +</html> diff --git a/src/main/resources/templates/lab/create/capacity.html b/src/main/resources/archive/lab/create/capacity.html similarity index 100% rename from src/main/resources/templates/lab/create/capacity.html rename to src/main/resources/archive/lab/create/capacity.html diff --git a/src/main/resources/templates/lab/create/components/advanced-lab.html b/src/main/resources/archive/lab/create/components/advanced-lab.html similarity index 100% rename from src/main/resources/templates/lab/create/components/advanced-lab.html rename to src/main/resources/archive/lab/create/components/advanced-lab.html diff --git a/src/main/resources/templates/lab/create/components/advanced-slotted.html b/src/main/resources/archive/lab/create/components/advanced-slotted.html similarity index 100% rename from src/main/resources/templates/lab/create/components/advanced-slotted.html rename to src/main/resources/archive/lab/create/components/advanced-slotted.html diff --git a/src/main/resources/templates/lab/create/components/advanced.html b/src/main/resources/archive/lab/create/components/advanced.html similarity index 100% rename from src/main/resources/templates/lab/create/components/advanced.html rename to src/main/resources/archive/lab/create/components/advanced.html diff --git a/src/main/resources/templates/lab/create/components/advanced/lab.html b/src/main/resources/archive/lab/create/components/advanced/lab.html similarity index 100% rename from src/main/resources/templates/lab/create/components/advanced/lab.html rename to src/main/resources/archive/lab/create/components/advanced/lab.html diff --git a/src/main/resources/templates/lab/create/components/advanced/session.html b/src/main/resources/archive/lab/create/components/advanced/session.html similarity index 100% rename from src/main/resources/templates/lab/create/components/advanced/session.html rename to src/main/resources/archive/lab/create/components/advanced/session.html diff --git a/src/main/resources/templates/lab/create/components/advanced/slotted.html b/src/main/resources/archive/lab/create/components/advanced/slotted.html similarity index 100% rename from src/main/resources/templates/lab/create/components/advanced/slotted.html rename to src/main/resources/archive/lab/create/components/advanced/slotted.html diff --git a/src/main/resources/templates/lab/create/components/lab-general.html b/src/main/resources/archive/lab/create/components/lab-general.html similarity index 100% rename from src/main/resources/templates/lab/create/components/lab-general.html rename to src/main/resources/archive/lab/create/components/lab-general.html diff --git a/src/main/resources/templates/lab/create/components/session-general.html b/src/main/resources/archive/lab/create/components/session-general.html similarity index 100% rename from src/main/resources/templates/lab/create/components/session-general.html rename to src/main/resources/archive/lab/create/components/session-general.html diff --git a/src/main/resources/archive/lab/create/exam.html b/src/main/resources/archive/lab/create/exam.html new file mode 100644 index 0000000000000000000000000000000000000000..cbee1acae5416603bfc85a0ae1e19f2f6fa33573 --- /dev/null +++ b/src/main/resources/archive/lab/create/exam.html @@ -0,0 +1,55 @@ +<!-- + + 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="~{lab/create/slotted}"> +<head> + <title>Create Exam Lab</title> +</head> + +<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"--> +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + +<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"--> +<!--@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="dto" type="nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO"--> +<!--@thymesVar id="lType" type="nl.tudelft.queue.model.enums.QueueSessionType"--> + +<body> +<th:block th:object="${dto}"> + <section layout:fragment="exam-config"> + <h4>Exam</h4> + <hr/> + + <div class="form-group form-row"> + <label for="exam-percentage-input" class="col-sm-2 col-form-label">Percentage for Exam lab</label> + <div class="col-sm-4"> + <input id="exam-percentage-input" th:field="*{examLabConfig.percentage}" + min="10" max="100" type="number" placeholder="15" class="form-control" + required/> + </div> + </div> + </section> +</th:block> + +</body> +</html> diff --git a/src/main/resources/templates/lab/create/regular.html b/src/main/resources/archive/lab/create/regular.html similarity index 100% rename from src/main/resources/templates/lab/create/regular.html rename to src/main/resources/archive/lab/create/regular.html diff --git a/src/main/resources/templates/lab/create/slotted.html b/src/main/resources/archive/lab/create/slotted.html similarity index 100% rename from src/main/resources/templates/lab/create/slotted.html rename to src/main/resources/archive/lab/create/slotted.html diff --git a/src/main/resources/templates/lab/edit.html b/src/main/resources/archive/lab/edit.html similarity index 100% rename from src/main/resources/templates/lab/edit.html rename to src/main/resources/archive/lab/edit.html diff --git a/src/main/resources/templates/lab/edit/capacity.html b/src/main/resources/archive/lab/edit/capacity.html similarity index 100% rename from src/main/resources/templates/lab/edit/capacity.html rename to src/main/resources/archive/lab/edit/capacity.html diff --git a/src/main/resources/templates/lab/edit/components/advanced-lab.html b/src/main/resources/archive/lab/edit/components/advanced-lab.html similarity index 100% rename from src/main/resources/templates/lab/edit/components/advanced-lab.html rename to src/main/resources/archive/lab/edit/components/advanced-lab.html diff --git a/src/main/resources/templates/lab/edit/components/advanced-slotted.html b/src/main/resources/archive/lab/edit/components/advanced-slotted.html similarity index 100% rename from src/main/resources/templates/lab/edit/components/advanced-slotted.html rename to src/main/resources/archive/lab/edit/components/advanced-slotted.html diff --git a/src/main/resources/templates/lab/edit/components/advanced.html b/src/main/resources/archive/lab/edit/components/advanced.html similarity index 100% rename from src/main/resources/templates/lab/edit/components/advanced.html rename to src/main/resources/archive/lab/edit/components/advanced.html diff --git a/src/main/resources/templates/lab/edit/components/advanced/lab.html b/src/main/resources/archive/lab/edit/components/advanced/lab.html similarity index 100% rename from src/main/resources/templates/lab/edit/components/advanced/lab.html rename to src/main/resources/archive/lab/edit/components/advanced/lab.html diff --git a/src/main/resources/templates/lab/edit/components/advanced/session.html b/src/main/resources/archive/lab/edit/components/advanced/session.html similarity index 100% rename from src/main/resources/templates/lab/edit/components/advanced/session.html rename to src/main/resources/archive/lab/edit/components/advanced/session.html diff --git a/src/main/resources/templates/lab/edit/components/advanced/slotted.html b/src/main/resources/archive/lab/edit/components/advanced/slotted.html similarity index 100% rename from src/main/resources/templates/lab/edit/components/advanced/slotted.html rename to src/main/resources/archive/lab/edit/components/advanced/slotted.html diff --git a/src/main/resources/templates/lab/edit/components/lab-general.html b/src/main/resources/archive/lab/edit/components/lab-general.html similarity index 100% rename from src/main/resources/templates/lab/edit/components/lab-general.html rename to src/main/resources/archive/lab/edit/components/lab-general.html diff --git a/src/main/resources/templates/lab/edit/components/session-general.html b/src/main/resources/archive/lab/edit/components/session-general.html similarity index 100% rename from src/main/resources/templates/lab/edit/components/session-general.html rename to src/main/resources/archive/lab/edit/components/session-general.html diff --git a/src/main/resources/templates/lab/edit/exam.html b/src/main/resources/archive/lab/edit/exam.html similarity index 100% rename from src/main/resources/templates/lab/edit/exam.html rename to src/main/resources/archive/lab/edit/exam.html diff --git a/src/main/resources/templates/lab/edit/regular.html b/src/main/resources/archive/lab/edit/regular.html similarity index 100% rename from src/main/resources/templates/lab/edit/regular.html rename to src/main/resources/archive/lab/edit/regular.html diff --git a/src/main/resources/templates/lab/edit/slotted.html b/src/main/resources/archive/lab/edit/slotted.html similarity index 100% rename from src/main/resources/templates/lab/edit/slotted.html rename to src/main/resources/archive/lab/edit/slotted.html diff --git a/src/main/resources/archive/lab/enqueue.html b/src/main/resources/archive/lab/enqueue.html new file mode 100644 index 0000000000000000000000000000000000000000..cc899c729411eb37f2fabb168fcefd5b4490da2c --- /dev/null +++ b/src/main/resources/archive/lab/enqueue.html @@ -0,0 +1,66 @@ +<!-- + + 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 di•••••••••••stributed 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="qSession" type="nl.tudelft.queue.model.QueueSession"--> +<!--@thymesVar id="request" type="nl.tudelft.queue.dto.create.RequestCreateDTO"--> + +<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"--> +<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"--> +<!--@thymesVar id="types" type="java.util.Map<java.lang.Long, java.util.Set<java.lang.String>>"--> + +<!--@thymesVar id="rType" type="java.lang.String"--> + +<!--@thymesVar id="message" type="java.lang.String"--> + +<body> +<section layout:fragment="subcontent"> + <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="page-sub-header"> + <h3 th:text="${'Session #' + qSession.id}"></h3> + </div> + + <div class="alert alert-info mt-md-3" role="alert" th:unless="${#strings.isEmpty(message)}"> + <span th:text="${message}"></span> + </div> + + <form th:action="@{/lab/{id}/enqueue/{rType}(id=${qSession.id}, rType=${rType})}" th:object="${request}" method="post" + class="form-horizontal"> + <th:block layout:fragment="enqueue-body"> + </th:block> + + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <button type="submit" name="enqueue" id="enqueue" + class="btn btn-success ctrl-enter-submit">Enqueue + </button> + <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> + </div> + </div> + </form> +</section> +</body> +</html> diff --git a/src/main/resources/templates/lab/enqueue/capacity.html b/src/main/resources/archive/lab/enqueue/capacity.html similarity index 100% rename from src/main/resources/templates/lab/enqueue/capacity.html rename to src/main/resources/archive/lab/enqueue/capacity.html diff --git a/src/main/resources/templates/lab/enqueue/lab.html b/src/main/resources/archive/lab/enqueue/lab.html similarity index 100% rename from src/main/resources/templates/lab/enqueue/lab.html rename to src/main/resources/archive/lab/enqueue/lab.html diff --git a/src/main/resources/templates/lab/enqueue/question.html b/src/main/resources/archive/lab/enqueue/question.html similarity index 100% rename from src/main/resources/templates/lab/enqueue/question.html rename to src/main/resources/archive/lab/enqueue/question.html diff --git a/src/main/resources/templates/lab/question.html b/src/main/resources/archive/lab/question.html similarity index 100% rename from src/main/resources/templates/lab/question.html rename to src/main/resources/archive/lab/question.html diff --git a/src/main/resources/archive/lab/remove.html b/src/main/resources/archive/lab/remove.html new file mode 100644 index 0000000000000000000000000000000000000000..7223294af27cf7865ec7da8130505883a9310824 --- /dev/null +++ b/src/main/resources/archive/lab/remove.html @@ -0,0 +1,51 @@ +<!-- + + 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="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"--> +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + +<!--@thymesVar id="qSession" type="nl.tudelft.queue.model.QueueSession"--> + +<body> +<section layout:fragment="subcontent"> + <div class="page-sub-header"> + <h3>Remove lab</h3> + </div> + + <form action="#" th:action="@{/lab/{id}/remove(id=${qSession.id})}" th:object="${qSession}" + class="form-horizontal" + method="post"> + <input type="hidden" th:field="*{id}"/> + + Are you sure you want to remove <strong th:text="${'session #' + qSession.id}"></strong>? + + <div class="text-center"> + <button class="btn btn-danger">Delete this session</button> + <small>or <a + th:href="@{/{ou}/{id}/labs(id=${ec == null ? edition.id : ec.id}, ou=${ec == null ? 'edition' : 'shared-edition'})}">go + back</a></small> + </div> + </form> +</section> +</body> +</html> diff --git a/src/main/resources/archive/lab/view.html b/src/main/resources/archive/lab/view.html new file mode 100644 index 0000000000000000000000000000000000000000..84dcdb866379ddeeff9c89b0ea9a5c92af796944 --- /dev/null +++ b/src/main/resources/archive/lab/view.html @@ -0,0 +1,136 @@ +<!-- + + 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="#authenticatedP" type="nl.tudelft.labracore.api.dto.Person"--> + +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + +<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.QueueSessionViewDTO"--> + +<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"--> +<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"--> +<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"--> + +<!--@thymesVar id="requests" type="java.util.List<nl.tudelft.queue.dto.view.RequestViewDTO>"--> + +<!--@thymesVar id="current" type="nl.tudelft.queue.dto.view.RequestViewDTO"--> +<!--@thymesVar id="currentDto" type="nl.tudelft.queue.dto.patch.RequestPatchDTO"--> + +<!--@thymesVar id="types" type="java.util.Map<java.lang.Long, java.util.Set<java.lang.String>>"--> + +<head> + <title th:text="|Lab ${qSession.id}|"></title> + + <script th:inline="javascript" type="text/javascript"> + //<![CDATA[ + const labId = /*[[${qSession.id}]]*/ -1; + //]]> + </script> + <script type="text/javascript" src="/js/lab_view.js"></script> + + <th:block th:if="${@permissionService.canManageSession(qSession.data)}"> + <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"/> + </th:block> +</head> + +<body> +<th:block layout:fragment="subcontent"> + <div class="page-sub-header"> + <div class="float-right"> + <th:block th:if="${current == null && @permissionService.canEnqueueSelf(qSession.id)}"> + <a href="#" th:href="@{/lab/{id}/enqueue(id=${qSession.id})}" + class="btn btn-success" style="margin: 0.25em" + th:classappend="${!@permissionService.canEnqueueSelfNow(qSession.id)} ? 'disabled'" + th:disabled="${!@permissionService.canEnqueueSelfNow(qSession.id)}">Enqueue</a> + </th:block> + + <div class="row" + th:if="${@permissionService.canManageSession(qSession.data)}"> + <a class="btn btn-primary" style="margin: 0.25em" + th:href="@{/lab/{id}/export(id=${qSession.id})}" download>Export lab</a> + <form class="form-inline" + th:action="@{/lab/{id}/close-enqueue/{bool}(id=${qSession.id}, bool=${!qSession.enqueueClosed})}" + method="post"> + <button th:unless="${qSession.enqueueClosed}" class="btn btn-danger" style="margin: 0.25em" + type="submit">Close enqueueing + </button> + <button th:if="${qSession.enqueueClosed}" class="btn btn-success" style="margin: 0.25em" + type="submit">Open enqueueing + </button> + </form> + + <a th:href="@{/lab/{id}/edit(id=${qSession.id})}" style="margin: 0.25em" class="btn btn-secondary"> + <i class="fa fa-pencil" aria-hidden="true"></i> + </a> + </div> + </div> + + <h3 th:text="'Lab #' + ${qSession.id} + ': ' + ${qSession.session.name}"></h3> + </div> + + <div class="row"> + <th:block th:replace="lab/view/components/selection-result :: selection-result"> + </th:block> + </div> + + <div th:if="${current != null}" + class="row"> + <th:block th:replace="lab/view/components/current-request :: current-request"> + </th:block> + + <th:block layout:fragment="current-request-edit"> + <th:block th:replace="lab/view/components/current-request-edit :: current-request-edit"> + </th:block> + </th:block> + </div> + + <div class="card-deck mt-3"> + <th:block layout:fragment="session-info"> + </th:block> + </div> + + <div class="mt-3"> + <th:block layout:fragment="request-table"> + </th:block> + </div> + + <script src="/js/map_loader.js"></script> + <script type="text/javascript"> + $(function () { + const roomId = $('select').val(); + if (roomId) { + updateRequestInfo(roomId); + } + }); + </script> +</th:block> + +<th:block layout:fragment="outside-content"> + <th:block layout:fragment="exam-request-table"> + </th:block> +</th:block> +</body> +</html> diff --git a/src/main/resources/templates/lab/view/capacity.html b/src/main/resources/archive/lab/view/capacity.html similarity index 100% rename from src/main/resources/templates/lab/view/capacity.html rename to src/main/resources/archive/lab/view/capacity.html diff --git a/src/main/resources/templates/lab/view/components/capacity-session-info.html b/src/main/resources/archive/lab/view/components/capacity-session-info.html similarity index 100% rename from src/main/resources/templates/lab/view/components/capacity-session-info.html rename to src/main/resources/archive/lab/view/components/capacity-session-info.html diff --git a/src/main/resources/templates/lab/view/components/current-request-edit.html b/src/main/resources/archive/lab/view/components/current-request-edit.html similarity index 100% rename from src/main/resources/templates/lab/view/components/current-request-edit.html rename to src/main/resources/archive/lab/view/components/current-request-edit.html diff --git a/src/main/resources/templates/lab/view/components/current-request.html b/src/main/resources/archive/lab/view/components/current-request.html similarity index 100% rename from src/main/resources/templates/lab/view/components/current-request.html rename to src/main/resources/archive/lab/view/components/current-request.html diff --git a/src/main/resources/templates/lab/view/components/exam-lab-info.html b/src/main/resources/archive/lab/view/components/exam-lab-info.html similarity index 100% rename from src/main/resources/templates/lab/view/components/exam-lab-info.html rename to src/main/resources/archive/lab/view/components/exam-lab-info.html diff --git a/src/main/resources/templates/lab/view/components/exam-lab-slots.html b/src/main/resources/archive/lab/view/components/exam-lab-slots.html similarity index 100% rename from src/main/resources/templates/lab/view/components/exam-lab-slots.html rename to src/main/resources/archive/lab/view/components/exam-lab-slots.html diff --git a/src/main/resources/templates/lab/view/components/full-request-table.html b/src/main/resources/archive/lab/view/components/full-request-table.html similarity index 100% rename from src/main/resources/templates/lab/view/components/full-request-table.html rename to src/main/resources/archive/lab/view/components/full-request-table.html diff --git a/src/main/resources/templates/lab/view/components/lab-info.html b/src/main/resources/archive/lab/view/components/lab-info.html similarity index 100% rename from src/main/resources/templates/lab/view/components/lab-info.html rename to src/main/resources/archive/lab/view/components/lab-info.html diff --git a/src/main/resources/templates/lab/view/components/selection-result.html b/src/main/resources/archive/lab/view/components/selection-result.html similarity index 100% rename from src/main/resources/templates/lab/view/components/selection-result.html rename to src/main/resources/archive/lab/view/components/selection-result.html diff --git a/src/main/resources/templates/lab/view/components/slots-info.html b/src/main/resources/archive/lab/view/components/slots-info.html similarity index 100% rename from src/main/resources/templates/lab/view/components/slots-info.html rename to src/main/resources/archive/lab/view/components/slots-info.html diff --git a/src/main/resources/templates/lab/view/components/slotted-lab-info.html b/src/main/resources/archive/lab/view/components/slotted-lab-info.html similarity index 100% rename from src/main/resources/templates/lab/view/components/slotted-lab-info.html rename to src/main/resources/archive/lab/view/components/slotted-lab-info.html diff --git a/src/main/resources/archive/lab/view/exam.html b/src/main/resources/archive/lab/view/exam.html new file mode 100644 index 0000000000000000000000000000000000000000..79eba6e8ac877e7ab63ae95e6c943af03f4bcf8c --- /dev/null +++ b/src/main/resources/archive/lab/view/exam.html @@ -0,0 +1,48 @@ +<!-- + + 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="~{lab/view/lab}"> + +<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.api.dto.Person"--> + +<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.labs.ExamLabViewDTO"--> +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + +<!--@thymesVar id="requests" type="java.util.List<nl.tudelft.queue.model.TimeSlot>"--> + +<!--@thymesVar id="current" type="nl.tudelft.queue.dto.view.RequestViewDTO"--> + +<head> + <title th:text="'Exam Lab #' + ${qSession.id}"></title> +</head> + +<body> +<th:block layout:fragment="additional-lab-info"> + <th:block th:replace="lab/view/components/exam-lab-info :: exam-lab-info"> + </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> +</th:block> +</body> +</html> diff --git a/src/main/resources/templates/lab/view/lab.html b/src/main/resources/archive/lab/view/lab.html similarity index 100% rename from src/main/resources/templates/lab/view/lab.html rename to src/main/resources/archive/lab/view/lab.html diff --git a/src/main/resources/archive/lab/view/regular.html b/src/main/resources/archive/lab/view/regular.html new file mode 100644 index 0000000000000000000000000000000000000000..4121caae9cfcacbbc5b051057bf84200ee4f4418 --- /dev/null +++ b/src/main/resources/archive/lab/view/regular.html @@ -0,0 +1,35 @@ +<!-- + + 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/>. + +--> +<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{lab/view/lab}"> + +<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.LabViewDTO"--> + +<head> + <title th:text="'Lab #' + ${qSession.id}"></title> +</head> + +<body> +<th:block layout:fragment="request-table"> + <th:block th:replace="lab/view/components/full-request-table :: full-request-table"> + </th:block> +</th:block> +</body> +</html> diff --git a/src/main/resources/archive/lab/view/slotted.html b/src/main/resources/archive/lab/view/slotted.html new file mode 100644 index 0000000000000000000000000000000000000000..85479108743166b35bbbecaab1d866d9cf9e0200 --- /dev/null +++ b/src/main/resources/archive/lab/view/slotted.html @@ -0,0 +1,45 @@ +<!-- + + 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/>. + +--> +<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{lab/view/lab}"> + +<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.labs.SlottedLabViewDTO"--> + +<head> + <title th:text="'Lab #' + ${qSession.id}"></title> +</head> + +<body> +<th:block layout:fragment="additional-lab-info"> + <th:block th:replace="lab/view/components/slotted-lab-info :: slotted-lab-info"> + </th:block> +</th:block> + +<th:block layout:fragment="request-table"> + <th:block th:replace="lab/view/components/slots-info :: slots-info"> + </th:block> + + <div class="mt-3"></div> + + <th:block th:replace="lab/view/components/full-request-table :: full-request-table"> + </th:block> +</th:block> +</body> +</html> diff --git a/src/main/resources/static/js/lab_status.js b/src/main/resources/archive/lab_status.js similarity index 100% rename from src/main/resources/static/js/lab_status.js rename to src/main/resources/archive/lab_status.js diff --git a/src/main/resources/static/js/lab_status_charts.js b/src/main/resources/archive/lab_status_charts.js similarity index 100% rename from src/main/resources/static/js/lab_status_charts.js rename to src/main/resources/archive/lab_status_charts.js diff --git a/src/main/resources/archive/lab_view.js b/src/main/resources/archive/lab_view.js new file mode 100644 index 0000000000000000000000000000000000000000..1192b88ea51d2d3f48f5a87d7b693b7a25013b16 --- /dev/null +++ b/src/main/resources/archive/lab_view.js @@ -0,0 +1,34 @@ +/* + * 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/>. + */ +"use strict" + +/** + * Handles the creation of a websocket connection by subscribing to the relevant topics. + * @param client The STOMP Client object to connect with. + */ +function handleSocketCreation(client) { + client.subscribe(`/user/topic/lab/${labId}/position`, msg => { + const event = JSON.parse(msg.body); + + if (event["type"] === "position-update") { + $("#position").text(event["position"]); + } else if (event["type"] === "request-taken") { + location.reload(); + } + }); +} diff --git a/src/main/resources/archive/layout.html b/src/main/resources/archive/layout.html new file mode 100644 index 0000000000000000000000000000000000000000..7722dc4fd877670ad6e2cd3b31f33a460ab18a19 --- /dev/null +++ b/src/main/resources/archive/layout.html @@ -0,0 +1,241 @@ +<!-- + + 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"> +<head> + <meta charset="utf-8"/> + <meta http-equiv="X-UA-Compatible" content="IE=edge"/> + <meta http-equiv="Content-Language" content="en_GB"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + + <meta name="description" content="Queue system"/> + <meta name="author" content=""/> + <meta name="_csrf_header" th:content="${_csrf.headerName}"/> + <meta name="_csrf" th:content="${_csrf.token}"/> + + <link rel="icon" href="/favicon.ico"/> + + <title th:unless="${@thymeleafConfig.isTheDay()}">Queue</title> + <title th:if="${@thymeleafConfig.isTheDay()}">Stack</title> + <title>Queue</title> + + <link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/> + <link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/> + <link rel="stylesheet" href="/webjars/font-awesome/css/v4-shims.css"/> + <link rel="stylesheet" type="text/css" href="/css/global.css"/> + + <script src="/webjars/jquery/jquery.min.js"></script> + <script src="/webjars/jquery-cookie/jquery.cookie.js"></script> + <script src="/webjars/bootstrap/js/bootstrap.bundle.min.js"></script> + + <script src="/webjars/sockjs-client/sockjs.min.js"></script> + <script src="/webjars/stomp-websocket/stomp.min.js"></script> + + <script src="/js/global.js"></script> + + <script type="application/javascript"> + $.ajaxSetup({ + headers: { + "X-CSRF-TOKEN": $("meta[name='_csrf']").attr("content") + } + }); + </script> + + <script th:if="${#authenticatedP != null}" th:inline="javascript" type="application/javascript"> + //<![CDATA[ + const authenticatedId = /*[[${#authenticatedP.id}]]*/ -1; + //]]> + </script> +</head> + +<!--@thymesVar id="page" type="java.lang.String"--> + +<!--@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"> + <!-- 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> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-nav" + aria-controls="navbar-nav" aria-expanded="false" aria-label="Toggle navigation"> + <i class="fa fa-bars" style="color:white;" aria-hidden="true"></i> + </button> + <div class="collapse navbar-collapse" id="navbar-nav"> + <ul class="navbar-nav nav-pills mr-auto mt-2 mt-lg-0"> + <th:block th:if="${#authenticatedP != null}"> + <li class="nav-item"> + <a class="nav-link" + th:classappend="${page == 'my-courses' ? 'active' : ''}" + th:href="@{/}">My Courses</a></li> + <li class="nav-item"> + <a class="nav-link" + th:classappend="${page == 'catalog' ? 'active' : ''}" + th:href="@{/editions}">Catalog</a></li> + <li class="nav-item" th:if="${@permissionService.canViewRequests()}"> + <a class="nav-link" + th:href="@{/requests}" + th:classappend="${page == 'requests' ? 'active' : ''}">Requests</a></li> + <li class="nav-item" th:if="${@permissionService.isAdmin()}"> + <a class="nav-link" + th:href="@{/admin}" + th:classappend="${page == 'admin' ? 'active' : ''}">Admin</a> + </li> + </th:block> + </ul> + <!-- Specifically for logged in users to get to personal pages --> + <ul class="navbar-nav"> + <th:block th:if="${#authenticatedP != null}"> + <li class="nav-item dropdown"> + <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" + role="button" aria-haspopup="true" aria-expanded="false" + th:text="${#authenticatedP.displayName}"> + <span class="caret"></span> + </a> + <div class="dropdown-menu dropdown-menu-right"> + <a class="dropdown-item text-dark" th:href="@{/history}">Request history</a> + <a class="dropdown-item text-dark" + th:href="@{/feedback/{id}(id=${#authenticatedP.id})}" + th:if="${@permissionService.canViewOwnFeedback()}">Feedback</a> + <form th:action="@{/logout}" method="post"> + <button class="dropdown-item" type="submit">Logout</button> + </form> + </div> + </li> + </th:block> + <li class="nav-item" th:unless="${#authenticatedP != null}"> + <a class="nav-link" th:href="@{/login}">Login</a> + </li> + </ul> + </div> +</nav> + +<main class="flex-fill fluid-container"> + <!-- Announcement banners --> + <div class="fluid-container justify-content-center mt-3 mb-3 ml-3 mr-3"> + <th:block th:each="announcement : ${@announcementRepository.findActiveAnnouncements()}"> + <div class="alert fade d-none" role="alert" th:data-announcement-id="|alert-${announcement.id}|" + th:styleappend="|color: ${'#' + announcement.hexTextColour()}; background-color: ${'#' + announcement.hexBackgroundColour()};|" + th:classappend="${announcement.isDismissible ? 'alert-dismissible' : ''}"> + <strong th:text="${announcement.message}"></strong> + <button type="button" class="close" data-dismiss="alert" aria-label="Close" + th:if="${announcement.isDismissible}"> + <span aria-hidden="true">×</span> + </button> + </div> + </th:block> + </div> + + <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"> + <th:block layout:fragment="content" class="content"> + <p>Page content goes here</p> + </th:block> + </div> + </div> + <th:block layout:fragment="outside-content"> + </th:block> + <div class="modal fade" id="sessionExpiredModal" tabindex="-1" role="dialog"> + <div class="modal-dialog modal-dialog-centered" role="document"> + <div class="modal-content"> + <div class="modal-body"> + <h3>Your session has expired</h3> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-primary" data-dismiss="modal">Close + </button> + <a th:href="@{/login}" class="btn btn-primary">Login</a> + </div> + </div> + </div> + </div> +</main> + +<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="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> + <div class="col-4 col-sm-2"> + <a class="text-muted btn" th:href="@{/about}">About</a> + </div> + <div class="col-3 col-sm-2"> + <img th:src="@{${@instituteProperties.logo}}" + class="float-right pr-2 mr-0" + height="40px" + alt="Logo"/> + </div> + </div> + </div> + </div> + </div> + + <th:block th:if="${#authenticatedP != null}"> + <!-- Import push.js --> + <script src="/js/push.js"></script> + <script th:inline="javascript"> + /*<![CDATA[*/ + + let timeout = /*[[${#httpSession.getMaxInactiveInterval()}]]*/ '10'; + setTimeout(function() { + $('#sessionExpiredModal').modal('show') + }, parseInt(timeout) * 1000) + + /*]]>*/ + </script> + </th:block> + <script type="application/javascript"> + // Initialize the dismissed announcements cookie when it does not yet exist + if (typeof $.cookie("dismissed-announcements") === "undefined") { + $.cookie("dismissed-announcements", "[]", { path: "/" }) + } + + // For every alert, check whether it is dismissed. If an alert is not dismissed, show it + $(() => { + const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements")); + + $(".alert").each(function() { + const id = $(this).data("announcement-id"); + if (!dismissedAnnouncements.includes(id)) { + $(this).removeClass("d-none").addClass("show"); + } + }); + }); + + // Register an event listener that adds a dismissed announcement to the dedicated cookie + $(".alert").on("closed.bs.alert", function() { + const id = $(this).data("announcement-id"); + + const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements")); + dismissedAnnouncements.push(id); + + $.cookie("dismissed-announcements", JSON.stringify(dismissedAnnouncements), { path: "/" }); + }); + </script> +</footer> +</body> +</html> diff --git a/src/main/resources/archive/login.html b/src/main/resources/archive/login.html new file mode 100644 index 0000000000000000000000000000000000000000..128efa600bbae86661b3ba162c0c75992d039d63 --- /dev/null +++ b/src/main/resources/archive/login.html @@ -0,0 +1,63 @@ +<!-- + + 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="~{layout}"> +<head> +</head> + +<body> +<section layout:fragment="content"> + <div class="page-header"> + <h1>Login</h1> + </div> + + <div class="alert alert-danger" role="alert" th:if="${param.error}"> + <span>Invalid username and password.</span> + </div> + + <div class="alert alert-info" role="alert" th:if="${param.logout}"> + <span>You have been logged out.</span> + </div> + + <form action="#" th:action="@{/perform-login}" class="form-horizontal" method="post" id="login-form"> + <div class="form-group"> + <label for="input-username" class="col-sm-2 control-label">Username</label> + + <div class="col-sm-8"> + <input type="text" id="input-username" name="username" class="form-control" placeholder="Username"/> + </div> + </div> + <div class="form-group"> + <label for="input-password" class="col-sm-2 control-label">Password</label> + + <div class="col-sm-8"> + <input type="password" id="input-password" name="password" class="form-control" placeholder="Password"/> + </div> + </div> + <input type="hidden" name="password" value=""/> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <input type="submit" class="btn btn-primary" value="Login"/> + </div> + </div> + </form> +</section> +</body> +</html> diff --git a/src/main/resources/static/js/map_loader.js b/src/main/resources/archive/map_loader.js similarity index 100% rename from src/main/resources/static/js/map_loader.js rename to src/main/resources/archive/map_loader.js diff --git a/src/main/resources/archive/module/create.html b/src/main/resources/archive/module/create.html new file mode 100644 index 0000000000000000000000000000000000000000..6185fae1159f959de09f38a634bea3f3f77b3505 --- /dev/null +++ b/src/main/resources/archive/module/create.html @@ -0,0 +1,64 @@ +<!-- + + 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}"> +<head> + <title th:text="|Create Module for ${'#' + edition.id}|"></title> + + <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script> + <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/> +</head> + +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + +<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueModuleCreateDTO"--> + +<body> +<section layout:fragment="subcontent"> + <div class="page-header"> + <h3>Create Module</h3> + </div> + + <form th:with="action = @{/edition/{id}/modules/create(id=${edition.id})}" + th:action="${action}" th:object="${dto}" + class="form-horizontal" method="post"> + <input type="hidden" name="id" th:value="${edition.id}"> + + <div class="form-group form-row"> + <label for="title-input" class="col-md-2 col-form-label">Name:</label> + <div class="col-md-4"> + <input id="title-input" + type="text" class="form-control" + required="required" th:field="*{name}" + placeholder="Name of the module. For instance: Assignments"/> + </div> + + <div class="col-md-6"> + <button type="submit" class="btn btn-primary ctrl-enter-submit float-right"> + Create new module + </button> + <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> + </div> + </div> + </form> +</section> +</body> +</html> diff --git a/src/main/resources/archive/module/remove.html b/src/main/resources/archive/module/remove.html new file mode 100644 index 0000000000000000000000000000000000000000..e2ad302af292b3b86984c7ea78c14c1ec57da098 --- /dev/null +++ b/src/main/resources/archive/module/remove.html @@ -0,0 +1,47 @@ +<!-- + + 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 layout:fragment="subcontent"> + <div class="page-sub-header"> + <h3>Remove Module</h3> + </div> + + <form th:action="@{/module/{id}/remove(id=${_module.id})}" + class="form-horizontal" + method="post"> + Are you sure you want to remove <strong + th:text="${'module #' + _module.id + ' (' + _module.name + ')'}"></strong>? + <div class="text-center"> + <button class="btn btn-danger">Delete this Module</button> + <small>or <a + th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small> + </div> + </form> +</section> +</body> +</html> diff --git a/src/main/resources/templates/notification/index.html b/src/main/resources/archive/notification/index.html similarity index 100% rename from src/main/resources/templates/notification/index.html rename to src/main/resources/archive/notification/index.html diff --git a/src/main/resources/archive/pagination.html b/src/main/resources/archive/pagination.html new file mode 100644 index 0000000000000000000000000000000000000000..e9ff0a1e557e26b4e498ef5ab927a6ba707c0360 --- /dev/null +++ b/src/main/resources/archive/pagination.html @@ -0,0 +1,74 @@ +<!-- + + 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"> + +<!--@thymesVar id="page" type="org.springframework.data.domain.Page<java.lang.Object>"--> +<!--@thymesVar id="size" type="java.lang.Integer"--> +<!--@thymesVar id="url" type="java.lang.String"--> + +<body> +<nav th:if="${page.getTotalPages() > 1}" aria-label="Page navigation" th:fragment="pagination (page, size)" th:with="url=${#httpServletRequest.requestURI}"> + <ul class="pagination"> + <li th:classappend="${not page.isFirst()} ? '' : 'disabled'" class="page-item"> + <span th:if="${page.isFirst()}" class="page-link">«</span> + + <a th:if="${!page.isFirst()}" th:href="@{${url}(page=0, size=${page.getSize()})}" class="page-link">«</a> + </li> + + <th:block th:if="${page.getTotalPages() < size}"> + <li th:each="pageNumber : ${#numbers.sequence(0, page.getTotalPages()-1)}" + th:classappend="${pageNumber == page.getNumber()} ? 'active' : ''" class="page-item"> + <span th:if="${pageNumber == page.getNumber()}" th:text="${pageNumber+1}" class="page-link">1</span> + <a th:if="${pageNumber != page.getNumber()}" th:text="${pageNumber+1}" + th:href="@{${url}(page=${pageNumber}, size=${page.getSize()})}" class="page-link">1</a> + </li> + </th:block> + + <th:block th:unless="${page.getTotalPages() < size}"> + <th:block th:if="${page.getNumber() > size}"> + <li class="page-item"><a href="#" class="page-link">...</a></li> + </th:block> + + <th:block th:each="pageNumber : ${#numbers.sequence(page.getNumber() - size + 1, page.getNumber() + size - 1)}"> + <th:block th:if="${pageNumber >= 0 and pageNumber <= page.getTotalPages()-1}"> + <li th:classappend="${pageNumber == page.getNumber()} ? 'active' : ''" class="page-item"> + <span th:if="${pageNumber == page.getNumber()}" th:text="${pageNumber+1}" class="page-link">1</span> + <a th:if="${pageNumber != page.getNumber()}" th:text="${pageNumber+1}" + th:href="@{${url}(page=${pageNumber}, size=${page.getSize()})}" class="page-link">1</a> + </li> + </th:block> + </th:block> + + <th:block th:if="${page.getNumber() + size <= page.getTotalPages()-1}"> + <li class="page-item"><a href="#" class="page-link">...</a></li> + </th:block> + </th:block> + + <li th:classappend="${not page.isLast()} ? '' : 'disabled'" class="page-item"> + <span th:if="${page.isLast()}" class="page-link">»</span> + + <a th:if="${!page.isLast()}" + th:href="@{${url}(page=${page.getTotalPages()-1}, size=${page.getSize()})}" class="page-link">»</a> + </li> + </ul> +</nav> +</body> +</html> diff --git a/src/main/resources/templates/request/approve.html b/src/main/resources/archive/request/approve.html similarity index 100% rename from src/main/resources/templates/request/approve.html rename to src/main/resources/archive/request/approve.html diff --git a/src/main/resources/templates/request/forward.html b/src/main/resources/archive/request/forward.html similarity index 100% rename from src/main/resources/templates/request/forward.html rename to src/main/resources/archive/request/forward.html diff --git a/src/main/resources/archive/request/list.html b/src/main/resources/archive/request/list.html new file mode 100644 index 0000000000000000000000000000000000000000..9a1e67069eec968122872aef9c834d6ca01a0f8c --- /dev/null +++ b/src/main/resources/archive/request/list.html @@ -0,0 +1,118 @@ +<!-- + + 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="~{layout}"> + +<!--@thymesVar id="filter" type="nl.tudelft.queue.dto.util.RequestTableFilterDTO"--> + +<!--@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="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="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.RequestViewDTO>"--> +<!--@thymesVar id="requestCounts" type="java.util.Map<java.lang.Long, java.lang.Long>"--> + +<head> + <title>Requests</title> + + <script 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"/> +</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 active">Requests</li> + </ol> + </nav> + + <div class="row"> + <h1 class="col-12">Requests</h1> + </div> + + <div class="row mb-2"> + <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" + th:id="|get-next-${lab.id}|" + th:if="${lab.isActiveOrGracePeriod}" + th:classappend="${requestCounts.getOrDefault(lab.id, 0) == 0} ? 'disabled' : ''" + th:href="@{/requests/next/{labId}(labId = ${lab.id})}"> + <span th:text="${@thymeleafConfig.isTheDay()} ? 'Pop from' : 'Get next for'"></span> + <th:block th:text="${lab.name}">Lab name</th:block> + <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" + class="btn btn-sm btn-success ml-4">Unstack me please</button> + </form> + <form th:if="${@thymeleafConfig.isTheDay() && !@requestTableService.partakes()}" + method="post" th:action="@{/requests/iloveallqueuedevelopers}"> + <button type="submit" + class="btn btn-sm btn-success ml-4">Stack me please</button> + </form> + </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> + + <div class="justify-content-center"> + <th:block th:replace="pagination :: pagination (page=${requests}, size=3)"> + </th:block> + </div> +</section> +</body> +</html> + diff --git a/src/main/resources/templates/request/list/filters.html b/src/main/resources/archive/request/list/filters.html similarity index 100% rename from src/main/resources/templates/request/list/filters.html rename to src/main/resources/archive/request/list/filters.html diff --git a/src/main/resources/templates/request/list/request-table.html b/src/main/resources/archive/request/list/request-table.html similarity index 100% rename from src/main/resources/templates/request/list/request-table.html rename to src/main/resources/archive/request/list/request-table.html diff --git a/src/main/resources/templates/request/reject.html b/src/main/resources/archive/request/reject.html similarity index 100% rename from src/main/resources/templates/request/reject.html rename to src/main/resources/archive/request/reject.html diff --git a/src/main/resources/templates/request/status.html b/src/main/resources/archive/request/status.html similarity index 100% rename from src/main/resources/templates/request/status.html rename to src/main/resources/archive/request/status.html diff --git a/src/main/resources/archive/request/view.html b/src/main/resources/archive/request/view.html new file mode 100644 index 0000000000000000000000000000000000000000..800a77656b39bf1a318975a15204c6912921e307 --- /dev/null +++ b/src/main/resources/archive/request/view.html @@ -0,0 +1,132 @@ +<!-- + + 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="~{layout}"> + +<!--@thymesVar id="request" type="nl.tudelft.queue.dto.view.RequestViewDTO"--> +<!--@thymesVar id="prevRequests" type="java.util.List<nl.tudelft.queue.dto.view.RequestViewDTO>"--> + +<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> +<!--@thymesVar id="assistant" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"--> + +<head> + <title th:text="${'Request #' + request.id}"></title> + + <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"/> +</head> + +<body> +<section layout:fragment="~{breadcrumb}"> + <nav role="navigation" class="breadcrumbs"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li> + <li class="breadcrumb-item"><a th:href="@{/editions}">Courses</a></li> + <li class="breadcrumb-item active" th:text="${'Request #' + request.id}"></li> + </ol> + </nav> +</section> + +<section layout:fragment="content"> + <th:block layout:fragment="claim-button"> + </th:block> + + <div class="row"> + <div class="col-sm-4"> + <th:block layout:fragment="feedback"> + </th:block> + + <th:block layout:fragment="history"> + </th:block> + </div> + + <div class="col-sm-8"> + <th:block layout:fragment="request-info"> + </th:block> + </div> + + <script src="/js/map_loader.js"></script> + <script type="text/javascript" th:inline="javascript"> + const roomId = /*[[${request.room}]]*/ 0; + updateRequestInfo(roomId); + </script> + + <style> + .btn-group.feedback > .btn.focus { + transition: .5s; + outline: none !important; + box-shadow: none; + } + + .btn-group.feedback > .btn.active:nth-child(1) { + background: #C23A2C !important; + border: 1px solid #AD3427 !important; + } + + .btn-group.feedback > .btn.active:nth-child(2) { + background: #F78921 !important; + border: 1px solid #F67C09 !important; + } + + .btn-group.feedback > .btn.active:nth-child(3) { + background: #EAD234 !important; + border: 1px solid #E8CD1D !important; + } + + .btn-group.feedback > .btn.active:nth-child(4) { + background: #76D142 !important; + border: 1px solid #68C931 !important; + } + + .btn-group.feedback > .btn.active:nth-child(5) { + background: #489B52 !important; + border: 1px solid #408949 !important; + } + + .btn-group.feedback > .btn:hover:nth-child(1) { + background: #C23A2C !important; + border: 1px solid #AD3427 !important; + } + + .btn-group.feedback > .btn:hover:nth-child(2) { + background: #F78921 !important; + border: 1px solid #F67C09 !important; + } + + .btn-group.feedback > .btn:hover:nth-child(3) { + background: #EAD234 !important; + border: 1px solid #E8CD1D !important; + } + + .btn-group.feedback > .btn:hover:nth-child(4) { + background: #76D142 !important; + border: 1px solid #68C931 !important; + } + + .btn-group.feedback > .btn:hover:nth-child(5) { + background: #489B52 !important; + border: 1px solid #408949 !important; + } + </style> + </div> +</section> +</body> +</html> diff --git a/src/main/resources/templates/request/view/components/claim-button.html b/src/main/resources/archive/request/view/components/claim-button.html similarity index 100% rename from src/main/resources/templates/request/view/components/claim-button.html rename to src/main/resources/archive/request/view/components/claim-button.html diff --git a/src/main/resources/templates/request/view/components/feedback.html b/src/main/resources/archive/request/view/components/feedback.html similarity index 100% rename from src/main/resources/templates/request/view/components/feedback.html rename to src/main/resources/archive/request/view/components/feedback.html diff --git a/src/main/resources/templates/request/view/components/history.html b/src/main/resources/archive/request/view/components/history.html similarity index 100% rename from src/main/resources/templates/request/view/components/history.html rename to src/main/resources/archive/request/view/components/history.html diff --git a/src/main/resources/templates/request/view/components/lab-request-info.html b/src/main/resources/archive/request/view/components/lab-request-info.html similarity index 100% rename from src/main/resources/templates/request/view/components/lab-request-info.html rename to src/main/resources/archive/request/view/components/lab-request-info.html diff --git a/src/main/resources/templates/request/view/components/selection-request-info.html b/src/main/resources/archive/request/view/components/selection-request-info.html similarity index 100% rename from src/main/resources/templates/request/view/components/selection-request-info.html rename to src/main/resources/archive/request/view/components/selection-request-info.html diff --git a/src/main/resources/archive/request/view/lab.html b/src/main/resources/archive/request/view/lab.html new file mode 100644 index 0000000000000000000000000000000000000000..08ed3184beb6f95db14558a202765ff8d420ddbf --- /dev/null +++ b/src/main/resources/archive/request/view/lab.html @@ -0,0 +1,45 @@ +<!-- + + 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="~{request/view}"> + +<body> +<th:block layout:fragment="claim-button"> + <th:block th:replace="request/view/components/claim-button :: claim-button"> + </th:block> +</th:block> + +<th:block layout:fragment="feedback"> + <th:block th:replace="request/view/components/feedback :: feedback"> + </th:block> +</th:block> + +<th:block layout:fragment="history"> + <th:block th:replace="request/view/components/history :: history"> + </th:block> +</th:block> + +<th:block layout:fragment="request-info"> + <th:block th:replace="request/view/components/lab-request-info :: request-info"> + </th:block> +</th:block> +</body> +</html> diff --git a/src/main/resources/templates/request/view/selection.html b/src/main/resources/archive/request/view/selection.html similarity index 100% rename from src/main/resources/templates/request/view/selection.html rename to src/main/resources/archive/request/view/selection.html diff --git a/src/main/resources/archive/request_table.js b/src/main/resources/archive/request_table.js new file mode 100644 index 0000000000000000000000000000000000000000..df87bf80b77b1b01a714f1cdecdd6aefb14f4a5a --- /dev/null +++ b/src/main/resources/archive/request_table.js @@ -0,0 +1,219 @@ +/* + * 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/>. + */ + +/** + * Finds the selected elements or the options that are selected. + * @param select The select box to find options of. + * @returns {*} The selected options as an array. + */ +function findSelected(select) { + const selected = select.val(); + if (typeof selected === "undefined" || selected.length === 0) { + return select.find("option").map(function () { + return this.value; + }).toArray(); + } + return selected; +} + +/** + * Checks the info of a request to see whether it is conform to the currently active filtering options. + * This constant is created by directly applying a function context finding the currently selected options and + * creating a function to actually filter an incoming request. + * @type {function(any): boolean} A function to check incoming requests on whether they should be displayed. + */ +const inFilter = (() => { + let selectedLabs; + let selectedAssignments; + let selectedRooms; + let selectedStatuses; + let selectedTypes; + + $(() => { + // Look up the currently selected filter options + selectedLabs = findSelected($("#lab-select")); + selectedAssignments = findSelected($("#assignment-select")); + selectedRooms = findSelected($("#room-select")); + selectedStatuses = findSelected($("#status-select")); + selectedTypes = findSelected($("#request-type-select")); + }); + + // Return a function checking whether an incoming request passes the filters + return event => { + return selectedLabs.includes(event["labId"].toString()) && + selectedAssignments.includes(event["assignmentId"].toString()) && + selectedRooms.includes(event["roomId"].toString()) && + selectedStatuses.includes(event["status"]) && + selectedTypes.includes(event["requestType"]); + } +}).apply() + +/** + * Handles the creation of a websocket connection by subscribing to the relevant topics. + * @param client The STOMP Client object to connect with. + */ +function handleSocketCreation(client) { + client.subscribe("/user/topic/request-table", msg => { + const event = JSON.parse(msg.body); + if (event.type === "request-created" && inFilter(event)) { + prependToRequestTable(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"]); + break; + case "request-revoked": + decreaseGetNextCounter(event["labId"]); + break; + case "request-forwarded-to-any": + increaseGetNextCounter(event["labId"]); + break; + case "request-forwarded-to-person": + if (event["forwardedTo"] === authenticatedId) { + increaseGetNextCounter(event["labId"]); + } + break; + default: + console.log(`Could not classify message from web socket: ${event}`); + return; + } + } + }); +} + +/** + * 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 prependToRequestTable(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().prependTo("#request-table tbody").fadeIn(); +} + +/** + * 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. + */ +function updateStatus(id, status) { + // Change the status badge to display the new status + const statusSelector = selectStatus(id); + statusSelector.text(status); + statusSelector.removeClass(); + statusSelector.addClass("badge badge-pill bg-info text-white"); + + // Change the background color of the row based on the status + const rowSelector = selectRow(id); + rowSelector.removeClass(); + rowSelector.addClass("text-white"); + rowSelector.addClass(bgColorDict[status]); +} + +/** + * Updates the text saying who is currently assigned to a request with the given id. + * @param id {number} The id of the request to change. + * @param assigned {string} The name of the person who is now assigned to the request. + */ +function updateAssigned(id, assigned) { + const assignedSelector = selectAssigned(id); + assignedSelector.text(assigned); +} + +/** + * Decreases the lab get-next button counter and disables it if the counter drops to 0. + * @param labId The id of the lab to update the get-next button for. + */ +function decreaseGetNextCounter(labId) { + const lab = $(`#get-next-${labId}`); + const span = $(`#span-${labId}`); + + // language=RegExp + let counter = parseInt(span.text().match(/\d+/)[0]); + if (counter !== 0) { + counter = counter - 1; + span.text(`(${counter})`); + if (counter === 0) { + lab.addClass("disabled"); + span.hide(); + } + } +} + +/** + * Increases the lab get-next button counter and enables it if necessary. + * @param labId {number} The id of the lab to update the get-next button for. + */ +function increaseGetNextCounter(labId) { + const lab = $(`#get-next-${labId}`); + const span = $(`#span-${labId}`); + const count = parseInt(span.text().match(/\d+/)[0]) + 1; + + lab.removeClass("disabled"); + span.text(`(${count})`); + span.show(); +} + +/** + * Selects the row with the request with the given id on it. + * @param id {number} The id of the request to select. + * @returns {*|jQuery.fn.init|jQuery|HTMLElement} + */ +function selectRow(id) { + return $("#request-" + id); +} + +/** + * Selects the status span of the request with the given id. + * @param id {number} The id of the request to select. + * @returns {*|jQuery.fn.init|jQuery|HTMLElement} + */ +function selectStatus(id) { + return $("#status-" + id); +} + +/** + * Selects the assigned data of the request with the given id. + * @param id {number} The id of the request to select. + * @returns {*|jQuery.fn.init|jQuery|HTMLElement} + */ +function selectAssigned(id) { + return $("#assigned-" + id); +} + +/** + * A dictionary mapping statuses to a colour. + * @type {{REVOKED: string, NOTFOUND: string, FORWARDED: string, PROCESSING: string, PENDING: string, APPROVED: string, REJECTED: string}} + */ +bgColorDict = { + "PENDING": "bg-pending", + "PROCESSING": "bg-processing", + "APPROVED": "bg-approved", + "REJECTED": "bg-rejected", + "FORWARDED": "bg-forwarded", + "REVOKED": "bg-revoked", + "NOTFOUND": "bg-notfound" +}; diff --git a/src/main/resources/templates/shared-edition/create/create-shared-edition.html b/src/main/resources/archive/shared-edition/create/create-shared-edition.html similarity index 100% rename from src/main/resources/templates/shared-edition/create/create-shared-edition.html rename to src/main/resources/archive/shared-edition/create/create-shared-edition.html diff --git a/src/main/resources/archive/shared-edition/view.html b/src/main/resources/archive/shared-edition/view.html new file mode 100644 index 0000000000000000000000000000000000000000..6ba75a80df706ad97aaff8a645d3d82eb100ae55 --- /dev/null +++ b/src/main/resources/archive/shared-edition/view.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout}"> +<body> + <section layout:fragment="content"> + <section layout:fragment="breadcrum"> + <nav role="navigation" class="breadcrumbs"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li> + <li class="breadcrumb-item"> + <a th:href="@{/}">My Courses</a> + </li> + <li class="breadcrumb-item active" th:text="${collection.getName()}">TI1316</li> + </ol> + </nav> + </section> + + <div class="controls" th:if="${@permissionService.canManageSharedSessions(collection.id)}"> + <div class="controls_buttons"> + <button class="btn btn-primary" + onclick="showDialog('create-shared-session-dialog')">Create shared + session</button> + </div> + <div th:replace="~{shared-edition/view/create-session-dialog :: overlay}"></div> + </div> + + <div class="tabs"> + <button id="sessions-tab" class="active tab" onclick="switchTab('sessions')">Sessions + </button> + <button id="editions-tab" class="tab" onclick="switchTab('editions')">Editions</button> + <button id="staff-tab" class="tab" onclick="switchTab('staff')">Staff</button> + </div> + + <div th:replace="~{shared-edition/view/session-list :: tab}"></div> + + <div id="editions" class="hidden"> + <table class="table"> + <thead> + <tr> + <th>Course edition</th> + <th>Course code</th> + <th>Teachers</th> + </tr> + </thead> + <tbody> + <tr th:each="edition : ${editions}"> + <td><a 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:with="teacher = ${editionTeachers[edition.id].toString()}" + th:text="${teacher.substring(1, teacher.length() - 1)}"></td> + </tr> + </tbody> + </table> + </div> + + <div id="staff" class="hidden"> + <table class="table"> + <thead> + <tr class="table_header"> + <th>Name</th> + <th>Role</th> + </tr> + </thead> + <tbody> + <tr th:each="instance : ${roles}"> + <td th:text="${instance.key.displayName}"></td> + <td> + <ul> + <li th:each="r : ${instance.getValue()}" + th:text="|${r.getType().getValue()} (${@editionCacheManager.getOrThrow(r.id.editionId).course.name})|"> + </li> + </ul> + </td> + </tr> + </tbody> + </table> + </div> + + <script> + let currentTab = "sessions" + function switchTab(to) { + if (to !== currentTab) { + document.getElementById(to).classList.toggle("hidden"); + document.getElementById(currentTab).classList.toggle("hidden"); + document.getElementById(to + "-tab").classList.toggle("active"); + document.getElementById(currentTab + "-tab").classList.toggle("active"); + } + currentTab = to; + } + </script> + </section> +</body> +</html> diff --git a/src/main/resources/templates/shared-edition/view/create-session-dialog.html b/src/main/resources/archive/shared-edition/view/create-session-dialog.html similarity index 100% rename from src/main/resources/templates/shared-edition/view/create-session-dialog.html rename to src/main/resources/archive/shared-edition/view/create-session-dialog.html diff --git a/src/main/resources/templates/shared-edition/view/session-list.html b/src/main/resources/archive/shared-edition/view/session-list.html similarity index 100% rename from src/main/resources/templates/shared-edition/view/session-list.html rename to src/main/resources/archive/shared-edition/view/session-list.html diff --git a/src/main/resources/scss/stack.scss b/src/main/resources/archive/stack.scss similarity index 100% rename from src/main/resources/scss/stack.scss rename to src/main/resources/archive/stack.scss diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 9d08c83823ad46c58a84dceb4575923832c62708..e35bdf3568b32cab639899fd9c7bd9a554ee861c 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -20,3 +20,30 @@ queue.slot.order = The slot must open before it closes. user.username.notFound = The given username could not be found. user.username.participates = The user already participates in this course. + +role.teacher = Teacher +role.head_ta = Head TA +role.ta = TA +role.student = Student +role.teacher_ro = Teacher (read only) + +lab.type.regular = Regular +lab.type.exam = Exam +lab.type.slotted = Slotted +lab.type.capacity = Limited capacity + +request.status.pending = Pending +request.status.processing = Processing +request.status.approved = Approved +request.status.rejected = Rejected +request.status.forwarded = Forwarded +request.status.revoked = Revoked +request.status.selected = Selected +request.status.notselected = Not selected +request.status.notfound = Not found +request.status.notpicked = Not picked + +session.selection.procedure.fcfs = First come, first serve +session.selection.procedure.random = Purely random +session.selection.procedure.weighted_by_count = Random, but people that got selected less often are prioritised +session.selection.procedure.weighted_by_least_recent = Random, but people that got selected least recently are prioritised diff --git a/src/main/resources/scss/_components.scss b/src/main/resources/scss/_components.scss new file mode 100644 index 0000000000000000000000000000000000000000..8a562942a127ab0cbe16301d0ad6a20b4ab58bef --- /dev/null +++ b/src/main/resources/scss/_components.scss @@ -0,0 +1,1043 @@ +.title { + color: var(--title-colour); + font-family: var(--title-font); + font-size: clamp(var(--font-size-800), 6vw, var(--font-size-900)); + font-weight: 100; +} + +.subtitle { + color: var(--subtitle-colour); + font-family: var(--title-font); + font-size: clamp(var(--font-size-700), 4vw, var(--font-size-800)); + font-weight: 100; +} + +.subsubtitle { + color: var(--subtitle-colour); + font-family: var(--title-font); + font-size: clamp(var(--font-size-600), 3vw, var(--font-size-700)); + font-weight: 100; +} + +.tabs { + display: flex; + font-size: clamp(var(--font-size-300), 2vw, var(--font-size-500)); + overflow-x: auto; + position: relative; + + &::after { + background-color: var(--divider-colour); + bottom: 1px; + content: ""; + height: 1px; + position: absolute; + width: 100%; + } + + & > button, + & > a { + background: none; + border: none; + color: var(--text-colour); + cursor: pointer; + display: flex; + flex-direction: column; + min-width: clamp(8rem, 20vw, 12rem); + padding-block: 1rem; + position: relative; + text-align: center; + text-decoration: none; + + &::before { + background-color: var(--divider-colour); + bottom: 1px; + content: ""; + height: 1px; + position: absolute; + width: 100%; + } + + &:hover::after, + &:focus-visible::after, + &[data-active="true"]::after { + background-color: var(--interactive-background-colour); + content: ""; + bottom: 0; + height: 3px; + left: 0; + position: absolute; + width: 100%; + z-index: 1; + } + } +} + +.button { + --colour: var(--interactive-background-colour); + + background-color: var(--colour); + border: none; + border-radius: var(--radius-300); + color: var(--interactive-text-colour); + cursor: pointer; + font-size: var(--font-size-300); + text-align: center; + text-decoration: none; + white-space: nowrap; + + &:hover, + &:focus-visible { + background-color: var(--interactive-active-background-colour); + color: var(--colour); + } + + &[data-style="floating"] { + box-shadow: var(--shadow-300); + } + &[data-style="outlined"] { + border: 1px solid var(--colour); + } + &[data-style="text"] { + background: none; + border: none; + color: var(--link-inactive-colour); + font-size: var(--font-size-500); + &:hover, + &:focus-visible { + color: var(--link-active-colour); + } + } + + &[data-type="negative"] { + --colour: var(--interactive-negative-background-colour); + } + &[data-type="positive"] { + --colour: var(--interactive-positive-background-colour); + } + + &:disabled { + background-color: var(--interactive-disabled-background-colour); + border: none; + cursor: initial; + color: var(--interactive-disabled-text-colour); + } +} + +.heading { + background-color: var(--surface-header-background-colour); + color: var(--surface-header-text-colour); + font-size: var(--font-size-400); + font-weight: bold; + padding: 1rem; + & button { + font-weight: initial; + } +} + +.list { + & > li { + background-color: var(--surface-backgroud-colour); + color: var(--surface-text-colour); + font-size: var(--font-size-400); + padding: 1rem; + + &[data-selectable] { + cursor: pointer; + &:focus-within, + &:hover { + background-color: var(--interactive-background-colour); + * { + border-color: var(--interactive-text-colour); + color: var(--interactive-text-colour); + } + } + } + + & + li { + border-top: 1px solid var(--surface-divider-colour); + } + } +} + +.link { + background: none; + border: none; + color: var(--link-inactive-colour); + cursor: pointer; + text-decoration: none; + &:hover, + &:focus-visible { + color: var(--link-active-colour); + } +} + +.chip { + background: none; + border: 1px solid var(--text-secondary-colour); + border-radius: var(--radius-300); + color: var(--text-secondary-colour); + font-size: var(--font-size-200); + padding-inline: 0.25rem; + white-space: nowrap; + + &[data-type="positive"] { + border-color: var(--text-secondary-positive-colour); + color: var(--text-secondary-positive-colour); + } + &[data-type="negative"] { + border-color: var(--text-secondary-negative-colour); + color: var(--text-secondary-negative-colour); + } + &[data-type="greyed-out"] { + border-color: var(--text-secondary-disabled-colour); + color: var(--text-secondary-disabled-colour); + } + &[data-type="action"] { + border-color: var(--link-inactive-colour); + color: var(--link-inactive-colour); + cursor: pointer; + &:hover, + &:focus-visible { + border-color: var(--link-active-colour); + color: var(--link-active-colour); + } + } +} + +.table { + border-collapse: collapse; + font-size: var(--font-size-400); + + td, + th { + padding: 1rem; + text-align: left; + } + td { + background-color: var(--surface-backgroud-colour); + } + tr { + padding: 0; + } + tr + :where(tr) { + border-top: 1px solid var(--surface-divider-colour); + } + + &__actions { + align-items: center; + display: flex; + font-weight: initial; + justify-content: flex-end; + gap: 0.5rem; + } + + tr[data-selectable] { + &:hover, + &:focus-within { + td { + background-color: var(--interactive-background-colour); + } + * { + border-color: var(--interactive-text-colour); + color: var(--interactive-text-colour); + } + cursor: pointer; + } + } + + &__to_list { + @media (max-width: 48em) { + td:first-child { + padding-bottom: 0.25rem; + } + td + td { + padding-top: 0.25rem; + } + td:last-child { + padding-bottom: 1rem; + } + .table__actions { + justify-content: flex-start; + } + } + } + + &__to_cards { + @media (max-width: 48em) { + box-shadow: none; + + thead { + // Visually hidden + left: -100px; + max-width: 1px; + max-height: 1px; + position: absolute; + top: -100px; + } + + tbody { + display: flex; + flex-direction: column; + gap: 1rem; + } + + &.shadow tr { + box-shadow: var(--shadow-300); + } + + tr { + background-color: var(--surface-backgroud-colour); + border-top: none; + display: flex; + flex-direction: column; + padding: 1rem; + } + + td { + padding: 0; + } + + td:first-child { + font-size: var(--font-size-500); + } + td:nth-child(2) { + color: var(--text-muted-colour); + margin-bottom: 1rem; + } + td:nth-child(3) { + margin-bottom: 0.5rem; + } + .table__actions { + align-self: flex-end; + } + } + } +} + +.searchbox { + display: flex; + + input { + border: none; + border-radius: var(--radius-300) 0 0 var(--radius-300); + font-size: var(--font-size-300); + padding: 0.5rem 1rem; + &::placeholder { + color: var(--interactive-placeholder-colour); + opacity: 100%; + } + } + + button { + aspect-ratio: 1 / 1; + border: none; + border-radius: 0 var(--radius-300) var(--radius-300) 0; + background-color: var(--interactive-background-colour); + color: var(--interactive-text-colour); + cursor: pointer; + flex-shrink: 0; + padding: 0.5rem; + &:hover, + &:focus-visible { + background-color: var(--interactive-text-colour); + color: var(--interactive-background-colour); + } + } + + &[data-style="floating"] { + box-shadow: var(--shadow-300); + } +} + +.textfield { + background-color: var(--interactive-active-background-colour); + border: none; + border-radius: var(--radius-300) 0 0 var(--radius-300); + flex-grow: 1; + font-size: var(--font-size-300); + padding: 0.5rem 1rem; + text-align: left; + + &:where(input) { + white-space: nowrap; + } + + &::placeholder { + color: var(--interactive-placeholder-colour); + opacity: 100%; + } + + &[data-style="floating"] { + box-shadow: var(--shadow-300); + } + &[data-style="outlined"] { + border: 1px solid var(--interactive-outline-colour); + } + + &[data-show-valid]:invalid { + border-color: var(--interactive-invalid-colour); + } +} + +.select { + display: flex; + flex-direction: column; + position: relative; + + &__box { + display: flex; + + &[data-style="floating"] { + box-shadow: var(--shadow-300); + } + &[data-style="outlined"] { + .select__input { + border: 1px solid var(--interactive-outline-colour); + border-right: none; + } + } + } + + &__input { + background-color: var(--surface-backgroud-colour); + border: none; + border-radius: var(--radius-300) 0 0 var(--radius-300); + flex-grow: 1; + font-size: var(--font-size-300); + padding: 0.5rem 1rem; + text-align: left; + white-space: nowrap; + &[data-placeholder="true"] { + color: var(--interactive-placeholder-colour); + } + } + + &__button { + aspect-ratio: 1 / 1; + background-color: var(--interactive-background-colour); + border: none; + border-radius: 0 var(--radius-300) var(--radius-300) 0; + color: var(--interactive-text-colour); + flex-shrink: 0; + padding: 0.5rem; + } + &:hover, + &:focus-visible, + &:focus-within { + .select__button { + background-color: var(--interactive-active-background-colour); + color: var(--interactive-active-text-colour); + } + [data-style="outlined"] { + .select__button { + border: 1px solid var(--interactive-outline-colour); + border-left: none; + } + } + } + + &__options { + box-shadow: var(--shadow-400); + display: flex; + flex-direction: column; + font-size: var(--font-size-300); + left: 0; + max-height: 20rem; + position: absolute; + overflow: scroll; + top: 2.5rem; + transform: scaleY(0); + transform-origin: top; + transition: transform 150ms ease-in-out; + z-index: 10; + &:focus-within { + transform: scaleY(1); + } + + & > * { + background-color: var(--surface-backgroud-colour); + border: none; + text-align: left; + padding: 0.5rem 1rem; + white-space: nowrap; + &:hover, + &:focus { + background-color: var(--interactive-background-colour); + color: var(--interactive-text-colour); + } + &:first-child { + border-radius: var(--radius-300) var(--radius-300) 0 0; + } + &:last-child { + border-radius: 0 0 var(--radius-300) var(--radius-300); + } + } + } + &[data-multiple] &__options > *[data-selected]::after { + font-weight: bold; + content: "✓"; + margin-left: 0.5rem; + } + + select[data-show-valid]:invalid + .select__box * { + border-color: var(--interactive-invalid-colour); + } +} + +.pagination { + align-self: flex-start; + border-radius: var(--radius-300); + box-shadow: var(--shadow-300); + font-size: var(--font-size-300); + + & > ul { + display: flex; + } + + .page__link { + aspect-ratio: 1 / 1; + background-color: var(--interactive-text-colour); + border: none; + color: var(--interactive-background-colour); + cursor: pointer; + display: inline-block; + min-height: 1.5rem; + min-width: 1.5rem; + padding: 0.25rem; + text-align: center; + text-decoration: none; + + &:hover, + &:focus-visible { + background-color: var(--interactive-background-colour); + color: var(--interactive-text-colour); + } + } + + .page__item { + &:first-child .page__link { + border-radius: var(--radius-300) 0 0 var(--radius-300); + } + &:last-child .page__link { + border-radius: 0 var(--radius-300) var(--radius-300) 0; + } + } + + .page__item:where(:not(:hover, :focus-within, [data-active="true"]), [data-disabled="true"]) + + .page__item:where(:not(:hover, :focus-within, [data-active="true"]), [data-disabled="true"]) { + position: relative; + &::after { + background-color: var(--surface-divider-colour); + content: ""; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 1px; + } + } + + .page__item[data-active="true"] { + .page__link { + background-color: var(--interactive-active-text-colour); + color: var(--interactive-active-background-colour); + } + } + + .page__item[data-disabled="true"] { + .page__link { + background-color: var(--interactive-disabled-text-colour); + color: var(--interactive-disabled-background-colour); + cursor: initial; + } + } +} + +dialog { + .dialog { + background-color: var(--dialog-background-colour); + border-radius: var(--radius-300); + color: var(--text-colour); + display: flex; + flex-direction: column; + gap: 1rem; + left: 50%; + min-width: min(100vw, 20rem); + padding: 2rem; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + } + background: none; + border: none; + min-height: 100vh; + min-width: 100vw; + + &:not([open]) { + display: none; + } + + &::backdrop { + background-color: rgba(0 0 0 / 25%); + } + + .dialog__title :where(h1, h2, h3, h4, h5) { + color: var(--title-colour); + font-family: var(--title-font); + font-size: var(--font-size-700); + font-weight: 100; + } + + @media (max-width: 48em) { + .dialog { + left: 0; + min-height: 100%; + min-width: 100%; + padding: 0; + top: 0; + transform: none; + } + + .dialog__title { + align-items: center; + background-color: var(--header-background-colour); + color: var(--header-text-colour); + display: flex; + justify-content: space-between; + padding: 1rem; + + & :where(h1, h2, h3, h4, h5) { + color: inherit; + font-family: var(--body-font); + font-weight: normal; + } + + button { + background: none; + border: none; + color: var(--header-text-colour); + cursor: pointer; + &:hover, + &:focus-visible { + text-decoration: underline; + } + } + } + + .dialog__body { + padding: 1rem 2rem; + } + } +} + +.label { + color: var(--text-muted-colour); + font-size: var(--font-size-400); +} + +.form__label { + color: var(--text-muted-colour); + font-size: var(--font-size-300); +} + +.nav_list { + --align: left; + --width: 15rem; + + display: flex; + flex-direction: column; + font-size: var(--font-size-300); + min-width: var(--width); + + & button, + & a { + background-color: var(--interactive-text-colour); + border: none; + color: var(--interactive-background-colour); + cursor: pointer; + padding: 1rem; + min-width: 100%; + text-align: var(--align); + + &:hover, + &:focus-visible { + background-color: var(--interactive-active-text-colour); + color: var(--interactive-active-background-colour); + } + + &:where([data-valid="false"]) { + color: var(--interactive-invalid-colour); + &:hover, + &:focus-visible { + background-color: var(--interactive-invalid-colour); + } + } + + &:disabled { + background-color: var(--interactive-disabled-text-colour); + color: var(--interactive-disabled-background-colour); + cursor: initial; + } + } + & > [data-active="true"] > :where(button, a) { + background-color: var(--interactive-active-text-colour); + color: var(--interactive-active-background-colour); + } + + & > :first-child > :where(button, a) { + border-radius: var(--radius-300) var(--radius-300) 0 0; + } + & > :last-child > :where(button, a) { + border-radius: 0 0 var(--radius-300) var(--radius-300); + } + + :where(:not(:hover, :focus-visible, [data-active="true"]) + + :where(:not(:hover, :focus-visible, [data-active="true"])), :disabled) { + position: relative; + &::before { + background-color: var(--surface-divider-colour); + content: ""; + height: 1px; + left: 0; + position: absolute; + top: 0; + width: 100%; + } + } + + &[data-style="floating"] { + box-shadow: var(--shadow-300); + } + &[data-style="outlined"] { + border: 1px solid var(--interactive-outline-colour); + } + + &[data-direction="horizontal"] { + & button, + & a { + padding: 0.5rem 1rem; + } + + @media (min-width: 48.001rem) { + flex-direction: row; + + & *::before { + height: 100%; + width: 1px; + } + + & button, + & a { + min-width: initial; + padding: 0.5rem 1rem; + } + + & > :first-child > :where(button, a) { + border-radius: var(--radius-300) 0 0 var(--radius-300); + } + & > :last-child > :where(button, a) { + border-radius: 0 var(--radius-300) var(--radius-300) 0; + } + } + } +} + +.info_card { + background-color: var(--surface-backgroud-colour); + font-size: var(--font-size-400); +} + +.banner { + align-items: center; + animation: banner-load 250ms ease-out; + border-radius: var(--radius-300); + box-shadow: var(--shadow-300); + color: var(--banner-text-colour); + display: flex; + gap: 0.5rem; + font-size: var(--font-size-300); + font-weight: bold; + padding: 0.75rem 1rem; + transform-origin: top left; + + .fa-solid { + font-size: var(--font-size-500); + } + + &[data-type="info"] { + background-color: var(--banner-info-colour); + } + &[data-type="warning"] { + background-color: var(--banner-warning-colour); + } + &[data-type="error"] { + background-color: var(--banner-negative-colour); + } + &[data-type="check"] { + background-color: var(--banner-positive-colour); + } + + & [data-remove-banner] { + border: none; + background: none; + color: var(--banner-text-colour); + cursor: pointer; + } +} + +@keyframes banner-load { + 0% { + transform: scaleY(0); + } + 100% { + transform: scaleY(1); + } +} + +.tooltip { + position: relative; + button { + border: none; + background: none; + color: var(--interactive-background-colour); + cursor: pointer; + } + p { + background-color: var(--surface-backgroud-colour); + box-shadow: var(--shadow-400); + left: 0; + max-width: 100ch; + padding: 0.5rem; + position: absolute; + top: 1.75rem; + transition: transform 150ms ease-in-out; + transform: scale(0); + transform-origin: top left; + z-index: 10; + } + button:where(:hover, :focus) + p { + transform: scale(1); + } +} + +.file { + &__box { + display: flex; + + &[data-style="floating"] { + box-shadow: var(--shadow-300); + } + + &[data-style="outlined"] { + .file__input { + border: 1px solid var(--interactive-outline-colour); + border-right: none; + } + } + } + + &__input { + background-color: var(--surface-backgroud-colour); + border: none; + border-radius: var(--radius-300) 0 0 var(--radius-300); + cursor: pointer; + flex-grow: 1; + font-size: var(--font-size-300); + padding: 0.5rem 1rem; + text-align: left; + white-space: nowrap; + + &[data-placeholder="true"] { + color: var(--interactive-placeholder-colour); + } + } + + &__button { + aspect-ratio: 1 / 1; + background-color: var(--interactive-background-colour); + border: none; + border-radius: 0 var(--radius-300) var(--radius-300) 0; + color: var(--interactive-text-colour); + cursor: pointer; + flex-shrink: 0; + padding: 0.5rem; + } + + &__box:hover, + &__box:focus-visible, + &__box:focus-within { + .file__button { + background-color: var(--interactive-active-background-colour); + color: var(--interactive-active-text-colour); + } + + &[data-style="outlined"] { + .file__button { + border: 1px solid var(--interactive-outline-colour); + border-left: none; + } + } + } + + input[data-show-valid][type="file"]:invalid + .file__box * { + border-color: var(--interactive-invalid-colour); + } +} + +.breadcrumbs { + font-size: var(--font-size-400); + margin-bottom: 1rem; + a { + color: var(--link-inactive-colour); + text-decoration: none; + &:hover, + &:focus-visible { + color: var(--link-active-colour); + } + } + span { + color: var(--text-muted-colour); + } +} + +#toasts { + align-items: flex-end; + bottom: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + right: 1rem; + position: fixed; +} + +.toast { + align-items: center; + animation: toast 400ms cubic-bezier(0, 0, 1, 1.5); + border-radius: var(--radius-300); + box-shadow: var(--shadow-400); + color: var(--banner-text-colour); + display: flex; + gap: 1rem; + font-size: var(--font-size-500); + max-width: 30rem; + padding: 1.5rem; + text-decoration: none; + + &[data-disappear] { + transition: transform 500ms ease-in; + transform: translateX(200%); + } + + .fa-solid { + font-size: var(--font-size-700); + } + + &[data-type="info"] { + background-color: var(--banner-info-colour); + } + &[data-type="warning"] { + background-color: var(--banner-warning-colour); + } + &[data-type="error"] { + background-color: var(--banner-negative-colour); + } + &[data-type="check"] { + background-color: var(--banner-positive-colour); + } + + @media (max-width: 30rem) { + min-width: calc(100vw - 2rem); + max-width: calc(100vw - 2rem); + } +} + +@keyframes toast { + 0% { + transform: translateY(calc(100% + 1rem)); + } + 100% { + transform: translateY(0); + } +} + +@keyframes toast_disappear { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(200%); + } +} + +.jump_to_content { + background-color: white; + border-radius: var(--radius-300); + box-shadow: var(--shadow-400); + left: 1rem; + padding: 1rem; + position: fixed; + top: -100%; + + &:focus { + top: 1rem; + } +} + +.enumeration { + & > li { + margin-left: 1.5rem; + } +} + +.history { + li[data-type="neutral"]::marker { + color: var(--interactive-background-colour); + } + li[data-type="negative"]::marker { + color: var(--interactive-negative-background-colour); + } + li[data-type="positive"]::marker { + color: var(--interactive-positive-background-colour); + } + li[data-type="greyed-out"]::marker { + color: var(--text-muted-colour); + } +} + +.stars { + align-self: flex-start; + display: flex; + gap: 0; + + button { + background: none; + border: none; + color: #ebb345; + cursor: pointer; + font-size: var(--font-size-600); + } + + :where([data-filled="true"]) .fa-regular { + display: none; + } + :where([data-filled="false"]) .fa-solid { + display: none; + } + + &:where(:hover, :focus-within) { + & .fa-solid { + display: initial; + } + & .fa-regular { + display: none; + } + } + button:where(:hover, :focus-visible) ~ * { + & .fa-solid { + display: none; + } + & .fa-regular { + display: initial; + } + } +} diff --git a/src/main/resources/scss/_composition.scss b/src/main/resources/scss/_composition.scss new file mode 100644 index 0000000000000000000000000000000000000000..c20b649a9521cadab754409dce7e53976fd777db --- /dev/null +++ b/src/main/resources/scss/_composition.scss @@ -0,0 +1,35 @@ +@use "util"; + +@include util.screen-sizes(".flex") { + --gap: 1rem; + display: flex; + gap: var(--gap); +} + +@include util.screen-sizes(".flex_column") { + --gap: 1rem; + display: flex; + flex-direction: column; + gap: var(--gap); +} + +.grid { + --columns: 2; + --gap: 0; + + display: grid; + gap: var(--gap); + grid-template-columns: repeat(var(--columns), 1fr); + + @include util.screen-sizes(".full_width") { + grid-column: span var(--columns); + } +} + +.subgrid { + grid-template-columns: repeat(var(--columns), 1fr); + @supports (grid-template-columns: subgrid) { + grid-template-columns: subgrid; + } + display: grid; +} diff --git a/src/main/resources/scss/_footer.scss b/src/main/resources/scss/_footer.scss new file mode 100644 index 0000000000000000000000000000000000000000..f9337f5779ed8dd462f4dde3826e56945aa948d7 --- /dev/null +++ b/src/main/resources/scss/_footer.scss @@ -0,0 +1,29 @@ +.footer { + background-color: var(--surface-backgroud-colour); + + &__content { + align-items: center; + padding: 0.5rem 2rem; + display: flex; + justify-content: space-between; + margin-inline: auto; + max-width: var(--content-width); + + @media (max-width: 48em) { + align-items: start; + flex-direction: column-reverse; + gap: 0.5rem; + padding-top: 1rem; + } + } + + &__logo { + display: flex; + align-items: center; + } + + &__links { + display: flex; + gap: 2rem; + } +} diff --git a/src/main/resources/scss/_header.scss b/src/main/resources/scss/_header.scss new file mode 100644 index 0000000000000000000000000000000000000000..f89d0db3fd79bc971788f819c52c09a1bd03b2b4 --- /dev/null +++ b/src/main/resources/scss/_header.scss @@ -0,0 +1,144 @@ +.header { + background-color: var(--header-background-colour); + font-size: var(--font-size-500); + + &__content { + align-items: center; + display: flex; + justify-content: space-between; + margin-inline: auto; + max-width: var(--content-width); + padding: 1.5rem 2rem; + @media (max-width: 48em) { + align-items: flex-start; + flex-direction: column; + gap: 1rem; + } + } + + &__top { + align-self: stretch; + display: flex; + justify-content: space-between; + } + + &__logo { + color: var(--header-text-colour); + font-size: var(--font-size-700); + text-decoration: none; + } + + &__hamburger { + background: none; + border: none; + color: var(--header-text-colour); + cursor: pointer; + font-size: var(--font-size-700); + @media (min-width: 48em) { + display: none; + } + } + + &__controls { + display: flex; + justify-content: space-between; + flex-grow: 1; + + @media (max-width: 48em) { + flex-direction: column; + gap: 1rem; + } + } + &[data-collapsed="true"] .header__controls { + @media (max-width: 48em) { + display: none; + } + } + + &__links { + display: flex; + gap: 1rem; + margin-left: 2rem; + + & > * { + color: var(--header-text-colour); + text-decoration: none; + &:hover, + &:focus-visible, + &[data-active="true"] { + text-decoration: underline; + } + } + + @media (max-width: 48em) { + flex-direction: column; + margin-left: 0; + } + } + + &__right { + position: relative; + } + + &__user { + align-items: center; + border: none; + background: none; + color: var(--header-text-colour); + cursor: pointer; + display: flex; + gap: 0.5rem; + text-decoration: none; + &:hover:where(a), + &:focus-visible:where(a) { + text-decoration: underline; + } + } + + &__dropdown { + border-radius: var(--radius-300); + box-shadow: var(--shadow-400); + display: flex; + flex-direction: column; + font-size: var(--font-size-400); + position: absolute; + right: 0; + top: 2rem; + transform: scaleY(0); + transform-origin: top; + transition: transform 150ms ease-in-out; + + .header__user:focus + &, + &:focus-within { + transform: scaleY(1); + } + + & > *:first-child, + & > *:first-child button { + border-radius: var(--radius-300) var(--radius-300) 0 0; + } + & > *:last-child, + & > *:last-child button { + border-radius: 0 0 var(--radius-300) var(--radius-300); + } + + & a, + & button { + background-color: var(--surface-backgroud-colour); + border: none; + color: var(--interactive-background-colour); + cursor: pointer; + min-width: 100%; + padding: 8px; + text-align: left; + text-decoration: none; + white-space: nowrap; + + &:hover, + &:focus-visible { + background-color: var(--interactive-active-text-colour); + color: var(--interactive-active-background-colour); + } + } + } +} diff --git a/src/main/resources/scss/_reset.scss b/src/main/resources/scss/_reset.scss new file mode 100644 index 0000000000000000000000000000000000000000..d1b651e1b2cd822ac18dc1aee9996a008a279dc1 --- /dev/null +++ b/src/main/resources/scss/_reset.scss @@ -0,0 +1,50 @@ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +ul[role="list"], +ol[role="list"] { + list-style: none; +} + +html:focus-within { + scroll-behavior: smooth; +} + +body { + min-height: 100vh; + text-rendering: optimizeSpeed; + line-height: 1.5; +} + +img, +picture { + max-width: 100%; + display: block; +} + +input, +button, +textarea, +select { + font: inherit; +} + +@media (prefers-reduced-motion: reduce) { + html:focus-within { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/src/main/resources/scss/_util.scss b/src/main/resources/scss/_util.scss new file mode 100644 index 0000000000000000000000000000000000000000..99149efae5a0fe39a404fcd79f38ba20c707e094 --- /dev/null +++ b/src/main/resources/scss/_util.scss @@ -0,0 +1,47 @@ +@mixin screen-sizes($class) { + #{$class} { + @content; + } + + @media (max-width: 30em) { + #{$class}\:tiny { + @content; + } + } + @media (max-width: 48em) { + #{$class}\:small { + @content; + } + } + @media (max-width: 64em) { + #{$class}\:medium { + @content; + } + } + @media (max-width: 75em) { + #{$class}\:large { + @content; + } + } + + @media (min-width: 30.001em) { + #{$class}\:from_tiny { + @content; + } + } + @media (min-width: 48.001em) { + #{$class}\:from_small { + @content; + } + } + @media (min-width: 64.001em) { + #{$class}\:from_medium { + @content; + } + } + @media (min-width: 75.001em) { + #{$class}\:from_large { + @content; + } + } +} diff --git a/src/main/resources/scss/_utility.scss b/src/main/resources/scss/_utility.scss new file mode 100644 index 0000000000000000000000000000000000000000..2a0f20a593de78b56dcbea61d047cf76b4f101da --- /dev/null +++ b/src/main/resources/scss/_utility.scss @@ -0,0 +1,150 @@ +@use "util"; + +@include util.screen-sizes(".align_items_start") { + align-items: flex-start; +} +@include util.screen-sizes(".align_items_center") { + align-items: center; +} +@include util.screen-sizes(".align_items_stretch") { + align-items: stretch; +} + +.text_align_center { + text-align: center; +} + +@include util.screen-sizes(".flex_grow") { + flex-grow: 1; +} + +.flex_wrap { + flex-wrap: wrap; +} + +.auto_fill { + --min-size: 16rem; + grid-template-columns: repeat(auto-fill, minmax(var(--min-size), 1fr)); +} + +.space_between { + justify-content: space-between; +} + +.position_relative { + position: relative; +} + +@for $i from 2 through 10 { + @include util.screen-sizes(".span-#{$i}") { + grid-column: span #{$i}; + } +} + +@for $i from 1 through 10 { + @include util.screen-sizes(".columns-#{$i}") { + --columns: #{$i}; + } +} + +.font-100 { + font-size: var(--font-size-100); +} +.font-200 { + font-size: var(--font-size-200); +} +.font-300 { + font-size: var(--font-size-300); +} +.font-400 { + font-size: var(--font-size-400); +} +.font-500 { + font-size: var(--font-size-500); +} +.font-600 { + font-size: var(--font-size-600); +} +.font-700 { + font-size: var(--font-size-700); +} +.font-800 { + font-size: var(--font-size-800); +} +.font-900 { + font-size: var(--font-size-900); +} + +.text_muted { + color: var(--text-muted-colour); +} + +$sizes: ( + 0: 0, + 1: 0.125rem, + 2: 0.25rem, + 3: 0.5rem, + 4: 0.75rem, + 5: 1rem, + 6: 1.5rem, + 7: 2rem, +); +@each $amt, $size in $sizes { + @include util.screen-sizes(".gap-#{$amt}") { + gap: #{$size}; + } + + .margin-#{$amt} { + margin: #{$size}; + } + .margin_top-#{$amt} { + margin-top: #{$size}; + } + .margin_left-#{$amt} { + margin-left: #{$size}; + } + .margin_right-#{$amt} { + margin-right: #{$size}; + } + @include util.screen-sizes(".margin_bottom-#{$amt}") { + margin-bottom: #{$size}; + } + .margin_inline-#{$amt} { + margin-inline: #{$size}; + } + .margin_block-#{$amt} { + margin-block: #{$size}; + } + + .padding-#{$amt} { + padding: #{$size}; + } + .padding_inline-#{$amt} { + padding-inline: #{$size}; + } + .padding_block-#{$amt} { + padding-block: #{$size}; + } + @include util.screen-sizes(".padding_bottom-#{$amt}") { + padding-bottom: #{$size}; + } +} + +.rounded { + border-radius: var(--radius-300); +} + +.shadow { + box-shadow: var(--shadow-300); +} + +@include util.screen-sizes(".hidden") { + display: none !important; +} +@include util.screen-sizes(".visually_hidden") { + position: absolute; + top: -2vh; + left: -2vw; + max-height: 1px; + min-height: 1px; +} diff --git a/src/main/resources/scss/_vars.scss b/src/main/resources/scss/_vars.scss new file mode 100644 index 0000000000000000000000000000000000000000..7f7075dac3d8ab3322aa6d4a7e3ae8b804ba91a2 --- /dev/null +++ b/src/main/resources/scss/_vars.scss @@ -0,0 +1,107 @@ +:root { + --neutral-000: #000000; + --neutral-100: #646464; + --neutral-200: #808080; + --neutral-300: #9a9a9a; + --neutral-500: #b3b3b3; + --neutral-600: #cacaca; + --neutral-700: #eaeaea; + --neutral-800: #f7f7f7; + --neutral-900: #fafafa; + --neutral-1000: #ffffff; + + --primary-500: #00a8db; + + --secondary-400: #003a73; + + --red-300: #731c1c; + --red-500: #c3312f; + + --green-400: #178231; + --green-500: #21ba46; + + --orange-500: #eb7245; + + --title-font: "Roboto", sans-serif; + --body-font: "Noto Sans", sans-serif; + + --font-size-100: 0.5625rem; + --font-size-200: 0.6875rem; + --font-size-300: 0.75rem; + --font-size-400: 0.875rem; + --font-size-500: 1rem; + --font-size-600: 1.25rem; + --font-size-700: 1.5rem; + --font-size-800: 2rem; + --font-size-850: 2.25rem; + --font-size-900: 2.625rem; + + --shadow-300: 0 1px 3px rgba(0 0 0 / 25%); + --shadow-400: 0 2px 5px rgba(0 0 0 / 25%); + + --radius-300: 2px; + + --break-tiny: 30em; + --break-small: 48em; + --break-medium: 64em; + --break-large: 75em; + + --content-width: min(75rem, 100vw); + + --background-colour: var(--neutral-800); + --text-colour: var(--neutral-000); + --text-muted-colour: var(--neutral-200); + --text-secondary-colour: var(--secondary-400); + --text-secondary-positive-colour: var(--green-400); + --text-secondary-negative-colour: var(--red-300); + --text-secondary-disabled-colour: var(--neutral-200); + + --header-background-colour: var(--primary-500); + --header-text-colour: var(--neutral-1000); + + --surface-header-background-colour: var(--neutral-700); + --surface-header-text-colour: var(--neutral-000); + --surface-backgroud-colour: var(--neutral-1000); + --surface-text-colour: var(--neutral-000); + --surface-divider-colour: var(--neutral-600); + + --title-colour: var(--neutral-000); + --subtitle-colour: var(--neutral-000); + + --interactive-background-colour: var(--primary-500); + --interactive-text-colour: var(--neutral-1000); + --interactive-outline-colour: var(--neutral-600); + --interactive-invalid-colour: var(--red-500); + --interactive-placeholder-colour: var(--neutral-500); + --interactive-negative-background-colour: var(--red-500); + --interactive-negative-text-colour: var(--neutral-1000); + --interactive-positive-background-colour: var(--green-500); + --interactive-positive-text-colour: var(--neutral-1000); + --interactive-active-background-colour: var(--neutral-1000); + --interactive-active-text-colour: var(--primary-500); + --interactive-disabled-background-colour: var(--neutral-500); + --interactive-disabled-text-colour: var(--neutral-1000); + + --link-inactive-colour: var(--primary-500); + --link-active-colour: var(--secondary-400); + + --banner-text-colour: var(--neutral-1000); + --banner-info-colour: var(--primary-500); + --banner-positive-colour: var(--green-500); + --banner-negative-colour: var(--red-500); + --banner-warning-colour: var(--orange-500); + + --dialog-background-colour: var(--neutral-900); + + --divider-colour: var(--neutral-600); + + background-color: var(--background-colour); + color: var(--text-colour); + font-family: var(--body-font); + line-height: 1.5; +} + +@media (prefers-color-scheme: dark) { + :root { + } +} diff --git a/src/main/resources/scss/global.scss b/src/main/resources/scss/global.scss index 7bd7d3f71f749e939cb30b1f338d95c13a3393a7..bc20d1c6d310fa2a720431c01e9a492cef8fe8ce 100644 --- a/src/main/resources/scss/global.scss +++ b/src/main/resources/scss/global.scss @@ -1,1149 +1,26 @@ -/* - * 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/>. - */ -@import "variables"; +@import "reset"; +@import "vars"; -/** - * Class for the get-next buttons on the requests page. - */ -.btn-get-next { - /* Default colouring when not disabled or hovered over */ - & { - background-color: $primary !important; - border-color: $primary !important; - } +@import "composition"; +@import "utility"; +@import "components"; - /* Make the get-next button gray if it is disabled */ - &.disabled { - background-color: $primary-disabled !important; - border-color: $primary-disabled !important; - } - - /* When hovering, use a different colour too */ - &:hover { - background-color: $primary-hover !important; - border-color: $primary-hover !important; - } -} - -.btn.disabled { - background-color: $primary-disabled !important; - border-color: $primary-disabled !important; -} - -.btn-outline-info.disabled { - color: white !important; -} - -/** - * A class for the progress bar used to signify capacity leftover on time slots. - */ -.timeslot-progress { - display: inline-flex; - left: -.63rem; - width: 115%; - height: 8px; - position: relative; -} - -/** - * A class used for showing a radio button as though it is a full div. - */ -.div-radio { - /* Disallow showing the radio button when the div is the button */ - & > input { - visibility: hidden; - position: absolute; - } - - /* Make the cursor a pointer when aiming at the target radio div */ - & > input + div { - cursor: pointer; - text-align: center; - } - - /* When the input is hovered over, color the div */ - & > input + div:hover:enabled { - color: #fff; - background-color: $primary-hover; - } - - /* When the input is checked, make it a slightly different colour */ - & > input:checked + div { - color: #fff; - background-color: $timeslot-checked; - border-color: $timeslot-checked; - } -} - -.pagination { - .active span { - background-color: $pagination-bg-active !important; - border-color: $pagination-bg-active !important; - } - - .disabled span { - background-color: $pagination-bg-disabled !important; - border-color: $pagination-bg-disabled !important; - color: $text-color-primary - } -} - -/** - * Boxed group - */ -.boxed-group { - margin-bottom: 20px; - - h3 { - display: block; - padding: 9px 10px 10px; - margin: 0; - font-size: 14px; - line-height: 17px; - background-color: $background-secondary; - border: 1px solid $border-primary; - color: $text-color-primary; - border-bottom: 0; - border-radius: 3px 3px 0 0; - } - - .boxed-group-inner { - padding: 10px; - font-size: 13px; - border: 1px solid $border-primary; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; - } - - .list-group-item:first-child { - border-top-left-radius: 0; - border-top-right-radius: 0; - } - - .list-group-item li { - line-height: 2em; - } -} - -.select-menu-button { - padding-right: 15px; - padding-left: 15px; -} - -.table-list { - display: table; - width: 100%; - color: $text-color-ternary; - table-layout: fixed; - border-bottom: 1px solid $border-inverse; - padding-left: 0; -} - -.table-list-item { - position: relative; - display: table-row; - list-style: none; -} - -.table-list-cell { - position: relative; - display: table-cell; - padding: 8px 10px; - font-size: 12px; - vertical-align: top; - border-top: 1px solid $border-inverse; - - &.status { - width: 90px; - } - - &:first-child { - border-left: 1px solid $border-inverse; - } - - &:last-child { - border-right: 1px solid $border-inverse; - } -} - -a.filters-reset { - color: $text-color-ternary; - - &:hover { - color: $primary; - text-decoration: none; - } -} - -.filters button { - background-color: $button-background; - color: $button-color; -} - -/** - * Panel box - */ -.mini-box { - min-height: 120px; - padding: 25px; - - .btn-icon { - margin: 0 15px 0 0; - font-size: 32px; - } - - .box-info { - display: inline-block; - vertical-align: top; - } -} - -.panel { - margin-bottom: 20px; - background-color: $background-secondary; - border: 1px solid transparent; - border-radius: 2px; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); -} - -.btn-icon { - display: inline-block; - text-align: center; - border-radius: 2px; - height: 35px; - width: 35px; - line-height: 35px; -} - -.btn-icon-round { - border-radius: 50%; -} - -.btn-icon-lg-alt { - height: 70px; - width: 70px; - line-height: 70px; -} - -.bg-info { - background-color: $primary; - color: $text-color-primary; -} - -/** - * Misc - */ -.table td.fit, -.table th.fit { - white-space: nowrap; - width: 1%; -} - -dl.dl-horizontal dd { - margin-bottom: 10px; -} - -#queueGraph { - width: 100%; - height: 300px; -} - -.input-daterange .input-group-addon { - border-left: none; - border-right: none; -} - -div.row-xs { - padding: 15px; - - &:nth-of-type(odd) { - background-color: $background-primary; - } -} - -.time { - font-size: 12px; - color: $text-color-ternary; - margin: 5px 0 0; -} - -/** - * Timeline - */ -.info-offset-left { - margin-left: 50px; -} - -.timeline-header { - text-transform: uppercase; - font-size: 12px; - color: $text-color-ternary; -} - -.timeline { - clear: both; - list-style: none; - padding: 0; - margin: 0; - font-size: 13px; - - li { - padding-bottom: 24px; - margin-left: 50px; - position: relative; - - &::before { - top: 0; - bottom: 0; - position: absolute; - content: " "; - width: 1px; - background-color: $border-primary; - left: -40px; - margin-left: 1px; - } - - &:first-child::before { - top: 10px; - } - - &:last-child:before { - top: 0; - bottom: 0; - height: 10px; - } - } - - .timeline-icon { - position: absolute; - top: 6px; - left: -46px; - z-index: 100; - margin: 0; - padding: 0; - - &::before { - content: " "; - position: absolute; - display: block; - width: 15px; - height: 15px; - border-radius: 50%; - border: 3px solid $background-primary; - margin: 0; - top: 0; - left: 0; - } - - // Request Colours - &.request-created::before { - background-color: $background-pending; - } - - &.request-revoked::before { - background-color: $background-revoked; - } - - &.request-approved::before { - background-color: $background-approved; - } - - &.request-selected::before { - background-color: $background-approved; - } - - &.request-taken::before { - background-color: $background-processing; - } - - &.request-forwarded::before { - background-color: $background-forwarded; - } - - &.request-rejected::before { - background-color: $background-rejected; - } - - &.request-not-selected::before { - background-color: $background-rejected; - } - - &.request-notfound::before { - background-color: $background-notfound; - } - } - - .timeline-description { - color: $text-color-secondary; - } - - .timeline-time { - color: $text-color-ternary; - } -} - -/** - * Table - */ -.table-body { - border-radius: 4px; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .12); -} - -.table-group { - .table-row { - border-bottom: 1px solid #e5e5e5; - border-left: 1px solid #bfbfc1; - border-right: 1px solid #bfbfc1; - line-height: 44px; - font-size: 15px; - position: relative; - word-wrap: break-word; - - &:first-child { - border-top: 1px solid #bfbfc1; - } - - &:first-child, - &:first-child a, - &:last-child { - border-top-right-radius: 4px; - border-top-left-radius: 4px; - } - - &:last-child { - border-bottom: 1px solid $border-primary; - } - } -} - -/** - * Unread indicator as overlay on hamburger menu - */ -.unread-indicator { - position: absolute; - display: block; - width: 14px; - height: 14px; - background-color: $primary; - border: 2px solid #333; - border-radius: 50%; -} - -.navbar-inverse { - background-color: $navbar-background; - border-color: $border-inverse; - color: $navbar-text; - - .navbar-brand, - .navbar-nav li a { - color: $navbar-text; - } -} - -.navbar-header .unread-indicator { - top: 0; - right: 0; -} - -.navbar-collapse .unread-indicator { - top: 10px; - right: 9px; -} - -/** - * List group - */ -.list-group .item-icon { - padding-left: 3em; - - span.icon { - position: absolute; - top: 25px; - left: 1em; - } -} - -.course-filter { - margin-bottom: 20px; - - .count { - float: right; - font-weight: bold; - } -} - -/** - * Truncation - */ -.truncate { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -/** - * Icon - */ -.icon.unread { - width: 12px; - height: 12px; - color: $text-color-inverse; - text-align: center; - background-image: -webkit-linear-gradient(lighten($primary, 20%), $primary); - background-image: linear-gradient(lighten($primary, 20%), $primary); - background-clip: padding-box; - border-radius: 50%; -} - -/** - * Pagehead - */ -.pagehead { - position: relative; - padding-top: 20px; - margin-top: -20px; - margin-bottom: 20px; - padding-bottom: 0; - background-color: $background-primary; - border-bottom: 1px solid $border-primary; - color: $text-color-primary; - - .nav-tabs { - border-bottom: 0; - } -} - -/** - * Page - */ -.page-sub-header { - padding-bottom: 9px; - margin: 20px 0 20px; -} - -.nav { - color: $navbar-text; -} - -.nav-tabs { - border-color: $navbar-tabs-border; - - .nav-link { - color: $navbar-tabs-color; - - &:hover, &:focus-visible { - border-color: $navbar-tabs-border; - } - - &.active { - color: $navbar-tabs-color; - border-color: $navbar-tabs-border; - background-color: $navbar-tabs-background; - } - } - - .fa { - margin-right: 5px; - } -} - -/** - * Lab list - */ -.list-group-item { - background-color: $background-primary; - border-color: $border-primary; - - &.lab-item { - line-height: 30px; - } -} - -/** - * Badge - */ -.badge { - color: #fff; -} - -/** - * Blankslate - */ -.blankslate { - position: relative; - padding: 30px; - text-align: center; - color: $text-color-primary; - background-color: $background-secondary; - border: 1px solid $border-primary; - border-radius: 3px; - box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); - - h3 { - margin-top: 0; - margin-bottom: 0; - font-size: 1.5em; - line-height: 1.43; - } - - p { - margin: 0; - } -} - -.breadcrumbs { - padding-top: 1em; -} - -.breadcrumb { - background: $background-secondary; -} - -.breadcrumb-item { - a { - color: $text-color-secondary; - } - - &.active { - color: $text-color-primary; - } -} - -.container-alert { - padding-top: 1em; -} - -.page-header { - margin-top: 1em; - margin-bottom: 1em; -} - -.bootstrap-datetimepicker-widget.dropdown-menu { - display: block; - margin: 2px 0; - padding: 4px; - width: 300px !important; -} - -.nav-pills .menu-link.active { - background: $navbar-selected !important; -} - -.nav-pills, .show > .nav-pills .nav-link { - background: none; -} - -.no-border-top th { - border-top: none !important; -} - -.sort-open { - display: flex; -} - -.lab-closed { - order: 1; -} - -.lab-open { - order: 0; -} - -/** - * Tables - */ -.table { - background-color: $background-primary; - color: $text-color-primary; - - th, td { - border-color: $background-secondary !important; - } -} - -/* - * Cards - */ -.card { - border-color: $border-primary; - - .card-header, .card-footer { - background-color: $background-primary; - } - - .card-body { - background-color: $background-secondary; - } - - &.queue-info { - color: $text-color-queue; - - .card-header, .card-footer { - background-color: $background-queue; - } - - .card-body { - background-color: $background-queue-secondary; - } - } - - &.location-info { - color: $text-color-location; - - .card-header, .card-footer { - background-color: $background-location; - } - - .card-body { - background-color: $background-location-secondary; - } - } - - &.lab-info { - color: $text-color-lab; - - .card-header, .card-footer { - background-color: $background-lab; - } - - .card-body { - background-color: $background-lab-secondary; - } - } - - &.more-info { - color: $text-color-more; - - .card-header, .card-footer { - background-color: $background-more; - } - - .card-body { - background-color: $background-more-secondary; - } - } - - &.approved { - color: $approved-text; - - .card-header, .card-footer { - background-color: $approved-primary; - } - - .card-body { - background-color: $approved-secondary; - } - } - - &.not-selected { - color: $not-selected-text; - - .card-header, .card-footer { - background-color: $not-selected-primary; - } - - .card-body { - background-color: $not-selected-secondary; - } - } -} - -/** - * Nav Tabs - */ - -/** - * Hacky solution to broken bootstrap styles when upgrading from 4.0.0 to 4.3.1 - */ -.btn-buttonface { - background-color: buttonface; -} - -/** - * Background colours - */ -.bg-processing { - background-color: $background-processing !important; -} - -.bg-rejected { - background-color: $background-rejected !important; -} - -.bg-not-selected, .bg-notselected { - background-color: $background-rejected !important; -} - -.bg-pending { - background-color: $background-pending !important; -} - -.bg-revoked { - background-color: $background-revoked !important; -} - -.bg-approved { - background-color: $background-approved !important; -} - -.bg-selected { - background-color: $background-approved !important; -} - -.bg-notfound { - background-color: $background-notfound !important; -} - -.bg-notpicked { - background-color: $background-notpicked !important; -} - -.bg-forwarded { - background-color: $background-forwarded !important; -} - -.btn-time-slot { - position: relative; - min-height: 50px; -} - -.progress-ts { - position: absolute; - bottom: 0; - height: 10px; - left: 0; - width: 100%; -} - -.table-tst { - & { - table-layout: fixed; - } - - & > thead > tr > td { - width: 25%; - } -} - -.tst-container { - position: relative; - - height: 120px; -} - -.tst-container-start { - @extend .empty-content; - top: 5%; - left: 0; - height: 90%; - width: 0.25rem; - - border-left-width: 1.5px; - border-left-style: solid; - border-left-color: #d7d7d7; -} - -.tst-container-end { - @extend .empty-content; - position: relative !important; - top: 5%; - left: 0; - height: 90%; - width: 0.25rem; - - border-right-width: 1.5px; - border-right-style: solid; - border-right-color: #d7d7d7; -} - -.tst-element { - &::after { - @extend .empty-content; - top: 50%; - left: 0; - height: 1px; - width: 100%; - } - - &:not(:first-of-type)::before { - @extend .empty-content; - top: 30%; - left: 0; - height: 40%; - width: 0.1rem; - } -} - -.tst-element-top { - position: absolute; - left: 5%; - top: 2%; - width: 90%; -} - -.tst-element-middle { - z-index: 1; - - position: absolute; - left: 10%; - top: 46.5%; - height: 10px; - width: 80%; -} - -.tst-element-bottom { - position: absolute; - left: 5%; - bottom: 9%; - width: 90%; -} - -.empty-content { - content: ""; - position: absolute; - background-color: #adadad; -} - -.text-hidden { - color: transparent; -} - -.active-filter { - background-color: $primary; -} - -dialog { - border: 1px solid var(--lt-color-gray-400); -} - -@mixin horizontal-bar($colour) { - background-color: $colour; - content: ''; - height: 1px; - left: 0; - position: absolute; - width: 100%; -} - -.tabs { - border-bottom: 1px solid var(--primary-grey); - display: flex; - font-size: var(--regular-size); - width: 100%; -} - -.tab { - background: none; - border: none; - color: var(--primary-dark); - cursor: pointer; - display: block; - padding-top: 1.8rem; - padding-bottom: 1.2rem; - position: relative; - text-align: center; - text-decoration: none; - width: 12.5rem; - - &::after { - @include horizontal-bar(var(--primary-blue)); - bottom: -2px; - height: 3px; - transform: scaleX(0); - z-index: 1; - } - &.active::after, &:hover::after { - transform: scaleX(1); - } - & + .tab { - margin-left: .5rem; - } -} - -.hidden { - display: none !important; -} - -.table { - border-collapse: collapse; - font-size: var(--table-size); - position: relative; - text-align: left; - width: 100%; - - --header-height: 1.4rem; - - box-shadow: 0 1px 3px rgb(0 0 0 / 25%); - - & > thead { - background-color: #EAEAEA; - th, td { - border-bottom: 3px solid var(--primary-dark); - border-top: 1px solid var(--primary-grey); - padding: var(--header-height) 0.5rem var(--header-height) 0; - } - th:first-child, td:first-child { - padding: var(--header-height) 0.5rem var(--header-height) var(--header-height); - } - th:last-child, td:last-child { - padding: var(--header-height) var(--header-height) var(--header-height) 0; - } - } - - tbody { - tr { - border-top: 1.5px solid var(--primary-grey); - border-radius: 2px; - gap: 6px; - - td { - vertical-align: initial; - } - - } - } - - &_actions { - display: flex; - justify-content: flex-end; - > * + * { - margin-left: 2rem; - } - > .icon-button + .icon-button { - margin-left: .5rem; - } - } - - &_cell-with-buttons { +.page { display: flex; - > span { - align-items: center; - display: flex; - } - > * + * { - gap: .5rem; - } - } - - td { - padding-right: 0.5rem; - } - td:first-child { - padding: 1rem 0.5rem 1rem var(--header-height); - } - td:last-child { - padding: 1rem var(--header-height) 1rem 0; - } - -} -.subtitle + .table { - margin-top: 1rem; + flex-direction: column; + min-width: 100%; + min-height: 100vh; } -.controls { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; - padding: 0.3rem 0 0.3rem 0; - - &_buttons { - margin-left: auto; - } -} - -.help-tip { - position: absolute; - display: inline-block; - text-align: center; - background-color: var(--primary-blue); - border-radius: 50%; - width: 24px; - height: 24px; - font-size: 12px; - line-height: 26px; - cursor: default; - transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); - margin-top: 0.4rem; - margin-left: 0.4rem; - outline: none; - &:hover, &:focus-visible { - cursor: pointer; - background-color: #ccc; - p { - cursor: default; - visibility: visible; - opacity: 1; - transform: scale(1.0); - } - } - .help-icon { - font-weight: 700; - color: #fff; - } - p { - visibility: hidden; - opacity: 0; - text-align: left; - background-color: var(--primary-blue); - padding: 20px; - width: 300px; - position: absolute; - border-radius: 4px; - right: -4px; - color: #fff; - font-size: 13px; - line-height: normal; - transform: scale(0.7); - transform-origin: 100% 0%; - transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); - z-index: 100; - &:before { - position: absolute; - content: ''; - width: 0; - height: 0; - border: 6px solid transparent; - border-bottom-color: var(--primary-blue); - right: 10px; - top: -12px; - } - &::after { - width: 100%; - height: 40px; - content: ''; - position: absolute; - top: -5px; - left: 0; - } - } - a { - color: #fff; - font-weight: 700; - &:hover, &:focus-visible { - color: #fff; - text-decoration: underline; - } - &:focus, &:focus-visible { - color: #fff; - text-decoration: underline; - } - } +.main { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 2rem; + margin-inline: auto; + max-width: var(--content-width); + min-width: var(--content-width); } +@import "header"; +@import "footer"; diff --git a/src/main/resources/static/js/announcement.js b/src/main/resources/static/js/announcement.js new file mode 100644 index 0000000000000000000000000000000000000000..71f8cd592d76967950ebdc569ec4cb74ecb5032b --- /dev/null +++ b/src/main/resources/static/js/announcement.js @@ -0,0 +1,26 @@ +$(() => { + // Initialize the dismissed announcements cookie when it does not yet exist + if (typeof $.cookie("dismissed-announcements") === "undefined") { + $.cookie("dismissed-announcements", "[]", { path: "/" }); + } + + // For every alert, check whether it is dismissed. If an alert is not dismissed, show it + const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements")); + + $(".alert").each(function () { + const id = $(this).data("announcement-id"); + if (!dismissedAnnouncements.includes(id)) { + $(this).removeClass("d-none").addClass("show"); + } + }); +}); + +// Register an event listener that adds a dismissed announcement to the dedicated cookie +$(".alert").on("closed.bs.alert", function () { + const id = $(this).data("announcement-id"); + + const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements")); + dismissedAnnouncements.push(id); + + $.cookie("dismissed-announcements", JSON.stringify(dismissedAnnouncements), { path: "/" }); +}); diff --git a/src/main/resources/static/js/file.js b/src/main/resources/static/js/file.js new file mode 100644 index 0000000000000000000000000000000000000000..8b8ed51692801cc7f73cb2d6c55d5647202017f9 --- /dev/null +++ b/src/main/resources/static/js/file.js @@ -0,0 +1,52 @@ +function setFileText(file) { + const actual = file.children("input"); + const files = actual.prop("files"); + if (files) { + file.find(".file__input") + .text([...files].map(f => f.name).join(",")) + .attr("data-placeholder", "false"); + } +} + +function openFileDialog(file) { + file.find("input[type='file']").click(); +} + +function configureFile(input) { + const parent = $(`<div class="file__box"> + <button type="button" class="file__input" data-placeholder="true">${input.data( + "placeholder" + )}</button> + <button type="button" class="file__button fa-solid fa-file"></button> + </div>`); + + parent.attr("data-style", input.attr("data-style")); + + input.addClass("hidden"); + setFileText(parent); + + input.after(parent); + parent.prepend(input); + + parent.find(".file__input").click(function () { + openFileDialog($(this).closest(".file__box")); + }); + parent.find(".file__button").click(function () { + openFileDialog($(this).closest(".file__box")); + }); + input.change(function () { + setFileText($(this).closest(".file__box")); + $(this).closest(".file__box").find("*:focus").blur(); + }); + input + .closest("form") + .find("[type='submit']") + .click(function () { + input.attr("data-show-valid", true); + }); +} + +$(() => { + const inputs = $("[data-file]"); + inputs.each((i, e) => configureFile($(e))); +}); diff --git a/src/main/resources/static/js/global.js b/src/main/resources/static/js/global.js index 8709e847b73769aeea4890478d8c7df729a54fef..629810d11174bbf600d7118fd0a2d4121a46c7bd 100644 --- a/src/main/resources/static/js/global.js +++ b/src/main/resources/static/js/global.js @@ -1,38 +1,37 @@ -/* - * 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/>. - */ -$(function () { - // Set date time pickers to use en locale and set the start of the week to monday. - if (typeof moment !== 'undefined') { //momentjs is only included on some pages, global.js on all - moment.locale('en', { - week: {dow: 1} // Monday is the first day of the week - }); +function openDialog(id, closeCurrent) { + if (closeCurrent) { + $("dialog[open]").each((i, d) => closeDialog(d.id)); } + document.getElementById(id).showModal(); + $(`#${id} :focus`).blur(); +} +function closeDialog(id) { + document.getElementById(id).close(); + const elem = $(`#${id}`); + if (elem.attr("data-remove-on-close") !== undefined) elem.remove(); +} - // Make sure all tooltip togglables will have a tooltip - $('[data-toggle="tooltip"]').tooltip(); +function configureDialog(dialog) { + dialog.find("[data-cancel]").click(function () { + closeDialog($(this).closest("dialog").attr("id")); + }); + if (dialog.is("[data-closable]")) + dialog.click(function (event) { + if (event.target !== this) return; + closeDialog($(this).attr("id")); + }); +} - // WebSocket for server sent events - connect(); -}); +function fetchAndOpenDialog(url) { + $.get(url, html => { + const element = $(html).filter((i, e) => $(e).is("dialog")); + $(".page").append(element); + openDialog(element.attr("id")); + configureDialog(element); + element.find("select").each((i, e) => configureSelect($(e))); + }); +} -/** - * Connects to the Queue server using a websocket. - */ function connect() { if (typeof handleSocketCreation === "undefined") { console.log("No socket handler defined, not opening a Web Socket"); @@ -57,109 +56,79 @@ function connect() { }); } -/* eslint-disable */ -// This code is not real, just like the cake -const _0x4543 = ['SErCi1bCjg==', 'w77Ct8OyXTI=', 'fEvDr8Ktwqxv', 'wqIRwrhwHA==', 'cRjDozkk', 'VQTDrMKwQ3cfaj9ZImQ8w4p0P37Di2rCtnoBak3Ci8Kzw5XDtsKvw7zCjMKHCHIiG1jCj8Kewr3CgMKNMcKCesKGw6I=', 'YsKOwoHCnw==', 'XsKNw6R/', 'RUISwqo=', 'w5zCimnCrg==', 'NcOALAw=', 'UcOiRcKZcA==', 'PxIewoI=', 'fMKCwoDCg10=', 'dMKFwod/w47CsMOe', 'OBIBwrXCqMOJwpI='];(function(_0x47b155, _0x59c74a){ - const _0x18eba6 = function (_0x45ab1c) { - while (--_0x45ab1c) { - _0x47b155['push'](_0x47b155['shift']()); - } - };_0x18eba6(++_0x59c74a);}(_0x4543,0xf6)); -const _0x2de6 = function (_0x126ee0, _0x53349) { - _0x126ee0 = _0x126ee0 - 0x0; - let _0x4ba7c7 = _0x4543[_0x126ee0]; - if (_0x2de6['xctFeP'] === undefined) { - (function () { - let _0x1d3bea; - try { - const _0x4a39c7 = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');'); - _0x1d3bea = _0x4a39c7(); - } catch (_0x29ff45) { - _0x1d3bea = window; - } - const _0x10d6bf = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - _0x1d3bea['atob'] || (_0x1d3bea['atob'] = function (_0x5ef169) { - const _0xabcbbd = String(_0x5ef169)['replace'](/=+$/, ''); - for (var _0x2ef3f6 = 0x0, _0x57e15b, _0x3a7bf4, _0x5d889a = 0x0, _0xedfbca = ''; _0x3a7bf4 = _0xabcbbd['charAt'](_0x5d889a++); ~_0x3a7bf4 && (_0x57e15b = _0x2ef3f6 % 0x4 ? _0x57e15b * 0x40 + _0x3a7bf4 : _0x3a7bf4, _0x2ef3f6++ % 0x4) ? _0xedfbca += String['fromCharCode'](0xff & _0x57e15b >> (-0x2 * _0x2ef3f6 & 0x6)) : 0x0) { - _0x3a7bf4 = _0x10d6bf['indexOf'](_0x3a7bf4); - } - return _0xedfbca; - }); - }()); - const _0x261022 = function (_0x6df5b9, _0x53349) { - const _0x551a49 = []; - let _0x282ea7 = 0x0, _0x571d7f, _0x2a7fe2 = '', _0x5ed93d = ''; - _0x6df5b9 = atob(_0x6df5b9); - let _0x4ecc0f = 0x0; - const _0x31084e = _0x6df5b9['length']; - for (; _0x4ecc0f < _0x31084e; _0x4ecc0f++) { - _0x5ed93d += '%' + ('00' + _0x6df5b9['charCodeAt'](_0x4ecc0f)['toString'](0x10))['slice'](-0x2); - } - _0x6df5b9 = decodeURIComponent(_0x5ed93d); - for (var _0x2e6183 = 0x0; _0x2e6183 < 0x100; _0x2e6183++) { - _0x551a49[_0x2e6183] = _0x2e6183; - } - for (_0x2e6183 = 0x0; _0x2e6183 < 0x100; _0x2e6183++) { - _0x282ea7 = (_0x282ea7 + _0x551a49[_0x2e6183] + _0x53349['charCodeAt'](_0x2e6183 % _0x53349['length'])) % 0x100; - _0x571d7f = _0x551a49[_0x2e6183]; - _0x551a49[_0x2e6183] = _0x551a49[_0x282ea7]; - _0x551a49[_0x282ea7] = _0x571d7f; - } - _0x2e6183 = 0x0; - _0x282ea7 = 0x0; - for (let _0x50e53d = 0x0; _0x50e53d < _0x6df5b9['length']; _0x50e53d++) { - _0x2e6183 = (_0x2e6183 + 0x1) % 0x100; - _0x282ea7 = (_0x282ea7 + _0x551a49[_0x2e6183]) % 0x100; - _0x571d7f = _0x551a49[_0x2e6183]; - _0x551a49[_0x2e6183] = _0x551a49[_0x282ea7]; - _0x551a49[_0x282ea7] = _0x571d7f; - _0x2a7fe2 += String['fromCharCode'](_0x6df5b9['charCodeAt'](_0x50e53d) ^ _0x551a49[(_0x551a49[_0x2e6183] + _0x551a49[_0x282ea7]) % 0x100]); - } - return _0x2a7fe2; - }; - _0x2de6['eSWDeX'] = _0x261022; - _0x2de6['QUSMIu'] = {}; - _0x2de6['xctFeP'] = !![]; - } - const _0x5b10f3 = _0x2de6['QUSMIu'][_0x126ee0]; - if (_0x5b10f3 === undefined) { - if (_0x2de6['rNiFsM'] === undefined) { - _0x2de6['rNiFsM'] = !![]; - } - _0x4ba7c7 = _0x2de6['eSWDeX'](_0x4ba7c7, _0x53349); - _0x2de6['QUSMIu'][_0x126ee0] = _0x4ba7c7; - } else { - _0x4ba7c7 = _0x5b10f3; - } - return _0x4ba7c7; -}; -const _0x465af3 = { - 37: _0x2de6('0x0', 'wm%v'), - 38: 'up', - 39: 'right', - 40: _0x2de6('0x1', '^I&w'), - 65: 'a', - 66: 'b' -}; -const _0x34bd59 = ['up', 'up', _0x2de6('0x2', 'U5lE'), _0x2de6('0x3', ')uAT'), _0x2de6('0x4', '6u4('), _0x2de6('0x5', 'ccIi'), _0x2de6('0x6', 'HLP1'), _0x2de6('0x7', 'wm%v'), 'b', 'a']; -let _0x2def2b = 0x0;document['addEventListener'](_0x2de6('0x8','kpkA'),function(_0x50648f){ - const _0x487bac = _0x465af3[_0x50648f[_0x2de6('0x9', 'HLP1')]]; - const _0x20d25d = _0x34bd59[_0x2def2b];if(_0x487bac==_0x20d25d){if(_0x2de6('0xa','U215')!==_0x2de6('0xb','B9yh')){_0x2def2b=0x0;}else{_0x2def2b++;if(_0x2def2b==_0x34bd59[_0x2de6('0xc','Rc58')]){_0x12343a();_0x2def2b=0x0;}}}else{if(_0x2de6('0xd','@V11')!==_0x2de6('0xe','zOV3')){_0x2def2b=0x0;}else{_0x2def2b++;if(_0x2def2b==_0x34bd59['length']){_0x12343a();_0x2def2b=0x0;}}}});function _0x12343a(){alert(_0x2de6('0xf','fn9d'));} -/* eslint-enable */ +function showToast(type, message) { + const icons = { + check: "fa-check-circle", + info: "fa-info-circle", + warning: "fa-exclamation-triangle", + error: "fa-exclamation-circle", + }; + const toast = $( + `<div class="toast" data-type="${type}"><span class="fa-solid ${icons[type]}"></span><p>${message}</p></div>` + ); -function countChar(val) { - var len = val.value.length - if (len > 500) { - val.value = val.value.substring(0, 500) - } else { - $('#charCount').text(len + "/500") - } -} + // Reanimate toasts + const toasts = $(".toast"); + toasts.css("animation", "none"); + toasts.each((i, e) => e.offsetHeight); + toasts.css("animation", ""); -function showDialog(id) { - document.getElementById(id).showModal(); -} + $("#toasts").append(toast); -function hideDialog(id) { - document.getElementById(id).close(); + setTimeout(() => toast.attr("data-disappear", "true"), 10000); + setTimeout(() => toast.remove(), 11000); } + +$(() => { + /* + * Tabs + */ + $("[data-tabs] [data-view]").click(function () { + $(this) + .closest("[data-tabs]") + .find("[data-view]") + .each((i, tab) => { + $(tab).attr("data-active", false); + $(`#${$(tab).data("view")}`).addClass("hidden"); + }); + $(this).attr("data-active", true); + $(`#${$(this).data("view")}`).removeClass("hidden"); + }); + + /* + * Dialog + */ + $("[data-dialog]").click(function () { + openDialog($(this).data("dialog"), $(this).data("close-current") !== undefined); + }); + $("[data-cancel]").click(function () { + closeDialog($(this).closest("dialog").attr("id")); + }); + $("[data-closable]").click(function (event) { + if (event.target !== this) return; + closeDialog($(this).attr("id")); + }); + + /* + * Trigger on stop typing + */ + $("textarea,input[type='text']").each((i, field) => { + let timeout = null; + $(field).keydown(function () { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => $(field).trigger("stopTyping"), 250); + }); + }); + + /* + * Banner + */ + $("[data-remove-banner]").click(function () { + $(this).closest(".banner").remove(); + }); + + /* + * Socket + */ + connect(); +}); diff --git a/src/main/resources/static/js/lab_view.js b/src/main/resources/static/js/lab_view.js index 1192b88ea51d2d3f48f5a87d7b693b7a25013b16..fd1fa0aac929d86a346d76e24d377ec8245f31ed 100644 --- a/src/main/resources/static/js/lab_view.js +++ b/src/main/resources/static/js/lab_view.js @@ -15,7 +15,7 @@ * 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/>. */ -"use strict" +"use strict"; /** * Handles the creation of a websocket connection by subscribing to the relevant topics. @@ -25,10 +25,15 @@ function handleSocketCreation(client) { client.subscribe(`/user/topic/lab/${labId}/position`, msg => { const event = JSON.parse(msg.body); + console.log(event); if (event["type"] === "position-update") { $("#position").text(event["position"]); + showToast("info", "You are now position " + event["position"] + " in Queue"); } else if (event["type"] === "request-taken") { - location.reload(); + $("#position").text("Assistant is coming"); + $("#unenqueue-button").prop("disabled", true); + showToast("info", "You are next in Queue! An assistant is on their way."); + new Notification("You are next in Queue! An assistant is on their way."); } }); } diff --git a/src/main/resources/static/js/push.js b/src/main/resources/static/js/push.js index 65c7cdfdf0aee2c5019b6f8073dc5ec727d5c9b0..1a2af4b7510e936adb17eee6dff109574e538568 100644 --- a/src/main/resources/static/js/push.js +++ b/src/main/resources/static/js/push.js @@ -31,13 +31,13 @@ $(function () { function initialiseState() { // Check whether the browser supports showing a notification in the first place if (!("showNotification" in ServiceWorkerRegistration.prototype)) { - console.warn("Notifications aren\"t supported."); + console.warn('Notifications aren"t supported.'); return; } // Check whether PushManager from the Push API is available in this browser if (!("PushManager" in window)) { - console.warn("Push messaging isn\"t supported."); + console.warn('Push messaging isn"t supported.'); return; } @@ -48,12 +48,11 @@ function initialiseState() { } // Ask the user for permission and send a message to the service worker to register the subscription. - Notification.requestPermission() - .then(permission => { - if (permission === "granted") { - subscribe(); - } - }); + Notification.requestPermission().then(permission => { + if (permission === "granted") { + subscribe(); + } + }); } /** @@ -68,9 +67,10 @@ function subscribe() { if (!subscription) { return fetch("/push/public-vapid-key") .then(response => response.text()) - .then(vapid => pm.subscribe({ + .then(vapid => + pm.subscribe({ userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(vapid) + applicationServerKey: urlBase64ToUint8Array(vapid), }) ); } @@ -88,7 +88,7 @@ function subscribe() { * @param subscription The subscription to send to Queue. */ function sendSubscriptionToServer(subscription) { - console.info(`Sending push subscription information to Queue: ${subscription.endpoint}`) + console.info(`Sending push subscription information to Queue: ${subscription.endpoint}`); const key = subscription.getKey ? subscription.getKey("p256dh") : ""; const auth = subscription.getKey ? subscription.getKey("auth") : ""; @@ -96,7 +96,7 @@ function sendSubscriptionToServer(subscription) { $.post("/push/subscribe", { endpoint: subscription.endpoint, p256dhKey: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : "", - auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : "" + auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : "", }); } @@ -106,10 +106,8 @@ function sendSubscriptionToServer(subscription) { * @param {string} base64String a public vapid key */ function urlBase64ToUint8Array(base64String) { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding) - .replace(/\-/g, '+') - .replace(/_/g, '/'); + const padding = "=".repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/"); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); diff --git a/src/main/resources/static/js/request_table.js b/src/main/resources/static/js/request_table.js index df87bf80b77b1b01a714f1cdecdd6aefb14f4a5a..eb82666435dea4ef70114c21187f0a7a641820be 100644 --- a/src/main/resources/static/js/request_table.js +++ b/src/main/resources/static/js/request_table.js @@ -24,9 +24,12 @@ function findSelected(select) { const selected = select.val(); if (typeof selected === "undefined" || selected.length === 0) { - return select.find("option").map(function () { - return this.value; - }).toArray(); + return select + .find("option") + .map(function () { + return this.value; + }) + .toArray(); } return selected; } @@ -41,7 +44,6 @@ const inFilter = (() => { let selectedLabs; let selectedAssignments; let selectedRooms; - let selectedStatuses; let selectedTypes; $(() => { @@ -49,19 +51,19 @@ const inFilter = (() => { selectedLabs = findSelected($("#lab-select")); selectedAssignments = findSelected($("#assignment-select")); selectedRooms = findSelected($("#room-select")); - selectedStatuses = findSelected($("#status-select")); selectedTypes = findSelected($("#request-type-select")); }); // Return a function checking whether an incoming request passes the filters return event => { - return selectedLabs.includes(event["labId"].toString()) && + return ( + selectedLabs.includes(event["labId"].toString()) && selectedAssignments.includes(event["assignmentId"].toString()) && selectedRooms.includes(event["roomId"].toString()) && - selectedStatuses.includes(event["status"]) && - selectedTypes.includes(event["requestType"]); - } -}).apply() + selectedTypes.includes(event["requestType"]) + ); + }; +}).apply(); /** * Handles the creation of a websocket connection by subscribing to the relevant topics. @@ -70,25 +72,31 @@ const inFilter = (() => { function handleSocketCreation(client) { client.subscribe("/user/topic/request-table", msg => { const event = JSON.parse(msg.body); + console.log(event); if (event.type === "request-created" && inFilter(event)) { - prependToRequestTable(event); - increaseGetNextCounter(event["labId"]) + appendToRequestTable(event); + increaseGetNextCounter(event["labId"]); } else if (event.type !== "request-created") { - updateStatus(event["id"], event["status"]); + updateStatus(event["id"], event["statusDisplayName"]); switch (event.type) { case "request-taken": decreaseGetNextCounter(event["labId"]); - updateAssigned(event["id"], event["takenBy"]); + if (event["takenById"] !== authenticatedId) { + removeRequest(event["id"]); + } break; case "request-revoked": decreaseGetNextCounter(event["labId"]); + removeRequest(event["id"]); break; case "request-forwarded-to-any": increaseGetNextCounter(event["labId"]); + prependToRequestTable(event); break; case "request-forwarded-to-person": if (event["forwardedTo"] === authenticatedId) { increaseGetNextCounter(event["labId"]); + prependToRequestTable(event); } break; default: @@ -99,10 +107,23 @@ function handleSocketCreation(client) { }); } +function setUpRequestRow(row) { + function fetchRequest() { + $.get(`/request/${$(this).data("request")}`, html => { + const element = $(html).filter((i, e) => $(e).is("dialog")); + $(".page").append(element); + openDialog(element.attr("id")); + configureDialog(element); + }); + } + row.click(fetchRequest); + row.find("[data-request]").click(fetchRequest); +} + /** * 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. + * @param event The event that occurred with information on the created request. */ function prependToRequestTable(event) { // Get the request template and fill it in @@ -111,7 +132,28 @@ function prependToRequestTable(event) { const html = template(event); // Add the compiled HTML to the request table with a fade in effect. - $(html).hide().prependTo("#request-table tbody").fadeIn(); + const current = $("#request-table [data-current='true']").last(); + const row = + current.length === 0 + ? $(html).hide().prependTo("#request-table tbody").fadeIn() + : $(html).hide().insertAfter(current).fadeIn(); + setUpRequestRow(row); +} + +/** + * 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 occurred 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. + const row = $(html).hide().appendTo("#request-table tbody").fadeIn(); + setUpRequestRow(row); } /** @@ -123,14 +165,7 @@ function updateStatus(id, status) { // Change the status badge to display the new status const statusSelector = selectStatus(id); statusSelector.text(status); - statusSelector.removeClass(); - statusSelector.addClass("badge badge-pill bg-info text-white"); - - // Change the background color of the row based on the status - const rowSelector = selectRow(id); - rowSelector.removeClass(); - rowSelector.addClass("text-white"); - rowSelector.addClass(bgColorDict[status]); + statusSelector.attr("data-type", ""); // TODO } /** @@ -143,24 +178,22 @@ function updateAssigned(id, assigned) { assignedSelector.text(assigned); } +function removeRequest(id) { + $(`#request-${id}-row`).fadeOut(); +} + /** * Decreases the lab get-next button counter and disables it if the counter drops to 0. * @param labId The id of the lab to update the get-next button for. */ function decreaseGetNextCounter(labId) { - const lab = $(`#get-next-${labId}`); - const span = $(`#span-${labId}`); - - // language=RegExp - let counter = parseInt(span.text().match(/\d+/)[0]); - if (counter !== 0) { - counter = counter - 1; - span.text(`(${counter})`); - if (counter === 0) { - lab.addClass("disabled"); - span.hide(); - } - } + const getNext = $(`#get-next-${labId}`); + const countLabel = getNext.find("[data-count]"); + const count = parseInt(countLabel.text()) - 1; + + countLabel.text(count.toString()); + + getNext.prop("disabled", count === 0); } /** @@ -168,13 +201,12 @@ function decreaseGetNextCounter(labId) { * @param labId {number} The id of the lab to update the get-next button for. */ function increaseGetNextCounter(labId) { - const lab = $(`#get-next-${labId}`); - const span = $(`#span-${labId}`); - const count = parseInt(span.text().match(/\d+/)[0]) + 1; + const getNext = $(`#get-next-${labId}`); + const countLabel = getNext.find("[data-count]"); + const count = parseInt(countLabel.text()) + 1; - lab.removeClass("disabled"); - span.text(`(${count})`); - span.show(); + getNext.prop("disabled", false); + countLabel.text(count.toString()); } /** @@ -189,10 +221,10 @@ function selectRow(id) { /** * Selects the status span of the request with the given id. * @param id {number} The id of the request to select. - * @returns {*|jQuery.fn.init|jQuery|HTMLElement} + * @return {*|jQuery} The status spans */ function selectStatus(id) { - return $("#status-" + id); + return $("#request-" + id + "-row").find("[data-status]"); } /** @@ -209,11 +241,11 @@ function selectAssigned(id) { * @type {{REVOKED: string, NOTFOUND: string, FORWARDED: string, PROCESSING: string, PENDING: string, APPROVED: string, REJECTED: string}} */ bgColorDict = { - "PENDING": "bg-pending", - "PROCESSING": "bg-processing", - "APPROVED": "bg-approved", - "REJECTED": "bg-rejected", - "FORWARDED": "bg-forwarded", - "REVOKED": "bg-revoked", - "NOTFOUND": "bg-notfound" + PENDING: "regular", + PROCESSING: "regular", + APPROVED: "positive", + REJECTED: "negative", + FORWARDED: "regular", + REVOKED: "greyed-out", + NOTFOUND: "regular", }; diff --git a/src/main/resources/static/js/select.js b/src/main/resources/static/js/select.js new file mode 100644 index 0000000000000000000000000000000000000000..603ff5aa5499d789b7175029d578bcf7a594b513 --- /dev/null +++ b/src/main/resources/static/js/select.js @@ -0,0 +1,151 @@ +function openSelect() { + if ($(this).closest(".select").data("open")) { + $(this).closest(".select").data("open", false); + return; + } + let option = $(this).closest(".select").find(".select__options > *[data-selected]"); + if (option.length === 0) + option = $(this).closest(".select").find(".select__options > *:first-child"); + $(option[0]).focus(); + $(this).closest(".select").data("open", true); +} + +function updateSelect(actual) { + const select = actual.closest(".select"); + select.find("[data-selected]").removeAttr("data-selected").data("selected", false); + actual.find("option:selected").each((i, opt) => + $(`[data-value="${$(opt).val()}"]`) + .attr("data-selected", "true") + .data("selected", true) + ); + const selectedOptions = select.find("[data-selected]"); + select + .find(".select__input") + .attr("data-placeholder", selectedOptions.length === 0) + .text( + selectedOptions.length === 0 + ? actual.data("placeholder") + : selectedOptions.length === 1 + ? selectedOptions.text() + : selectedOptions.length + " selected" + ); +} + +function setSelectText(select) { + const actual = select.children("select"); + const selectedOptions = select.find("[data-selected]"); + if (select.attr("data-multiple")) { + select + .find(".select__input") + .attr("data-placeholder", selectedOptions.length === 0) + .text( + selectedOptions.length === 0 + ? actual.data("placeholder") + : selectedOptions.length === 1 + ? selectedOptions.text() + : selectedOptions.length + " selected" + ); + } else if (selectedOptions.length > 0) { + select.find(".select__input").attr("data-placeholder", false).text(selectedOptions.text()); + } +} + +function selectOption() { + const select = $(this).closest(".select"); + const actual = select.children("select"); + + if (select.attr("data-multiple")) { + const selected = $(this).data("selected"); + $(this) + .data("selected", !selected) + .attr("data-selected", selected ? null : "true"); + const selectedOptions = select.find("[data-selected]"); + actual.val([...selectedOptions.map((i, opt) => $(opt).data("value"))]); + } else { + actual.val($(this).data("value")); + select.find("[data-selected]").removeAttr("data-selected"); + $(this).attr("data-selected", true); + $(this).blur(); + } + setSelectText(select); + actual.change(); +} + +function selectOptionNavigate(e) { + if (e.which === 38) { + // UP + $(this).prev().focus(); + } + if (e.which === 40) { + // DOWN + $(this).next().focus(); + } +} + +function selectOptionBlur() { + if (!$(this).closest(".select").is(":focus-within")) { + $(this).closest(".select").data("open", false); + } +} + +function configureSelect(select) { + const parent = $(`<div class="select hidden"> + <div class="select__box"> + <button type="button" class="select__input" data-placeholder="true">${select.data( + "placeholder" + )}</button> + <button type="button" class="select__button fa-solid fa-chevron-down"></button> + </div> + <div class="select__options"></div> + </div>`); + + const optionsBox = parent.children(".select__options"); + let options = select.find("option").filter((i, opt) => !opt.hasAttribute("data-empty-option")); + options.each((i, opt) => { + const option = $( + `<button tabindex="-1" type="button" data-value="${$(opt).val()}">${$( + opt + ).text()}</button>` + ); + const selected = $(opt).attr("selected") === "selected"; + option.data("selected", selected); + if (selected) option.attr("data-selected", true); + optionsBox.append(option); + }); + + const box = parent.children(".select__box"); + box.attr("data-style", select.attr("data-style")); + box.css( + "min-width", + "min(80vw, " + + (Math.max(15, ...$(options).map((i, opt) => $(opt).text().trim().length)) + 2 + "ch") + + ")" + ); + + select.addClass("hidden"); + if (select.attr("multiple")) parent.attr("data-multiple", true); + setSelectText(parent); + + select.after(parent); + parent.prepend(select); + parent.removeClass("hidden"); + + parent.find(".select__input").click(openSelect); + parent.find(".select__button").click(openSelect); + parent + .find(".select__options > *") + .keydown(selectOptionNavigate) + .click(selectOption) + .blur(selectOptionBlur); + select + .closest("form") + .find("[type='submit']") + .click(function () { + select.attr("data-show-valid", true); + }); +} + +$(() => { + const selects = $("[data-select]"); + selects.each((i, e) => configureSelect($(e))); +}); diff --git a/src/main/resources/static/js/session_wizard.js b/src/main/resources/static/js/session_wizard.js new file mode 100644 index 0000000000000000000000000000000000000000..c76bd09d4673ad451f0e99234fd95d9bbbda1056 --- /dev/null +++ b/src/main/resources/static/js/session_wizard.js @@ -0,0 +1,306 @@ +/** + * The form currently being displayed. + */ +let currentForm = "type"; + +/** + * The type of session being created. + */ +let sessionType = "REGULAR"; + +/** + * Determines whether we are editing, copying, or creating a session. + * In particular, determines whether we will show the 'select type' page and what http method is used. + * Values: create / copy / edit + */ +let wizardType = "create"; + +/** + * Determines what pages should be displayed in the wizard depending on the session type. + */ +const nextForms = { + REGULAR: { + type: "general", + general: "rooms", + rooms: "assignments", + assignments: "allowed_requests", + allowed_requests: "extra_settings", + }, + SLOTTED: { + type: "general", + general: "rooms", + rooms: "assignments", + assignments: "allowed_requests", + allowed_requests: "slots", + slots: "extra_settings", + }, + EXAM: { + type: "general", + general: "rooms", + rooms: "assignments", + assignments: "allowed_requests", + allowed_requests: "slots", + slots: "exam", + exam: "extra_settings", + }, + CAPACITY: { + type: "general", + general: "rooms", + rooms: "modules", + modules: "limited_capacity", + limited_capacity: "extra_settings", + }, +}; + +const dependencies = { + extra_settings: ["modules", "assignments"], + allowed_requests: ["assignments"], +}; + +/** + * Converts the name of a form to a human-readable format. + */ +function getSentence(name) { + return name.substring(0, 1).toUpperCase() + name.substring(1).replace("_", " "); +} + +/** + * Gets the form with the specified name. + */ +function getForm(name) { + return $(`[data-create-session-part="${name}"]`); +} + +/** + * Checks whether a form is valid. + */ +function isValid(form) { + const inputs = form.find(":where(input, select, textarea)"); + return [...inputs].every(e => e.validity.valid); +} + +/** + * Reports the invalid form elements to the user. + */ +function reportValid(form) { + const inputs = form.find(":where(input, select, textarea)"); + inputs.each((i, e) => e.reportValidity()); + form.find("select").attr("data-show-valid", true); +} + +/** + * Checks whether all dependencies to go to a form are met. + */ +function canGoToForm(name) { + if (dependencies[name] === undefined) return true; + const forms = getForms(); + return dependencies[name] + .filter(dep => forms.includes(dep)) + .every(dep => isValid(getForm(dep))); +} + +/** + * Gets the next form name. + */ +function getNextForm() { + return nextForms[sessionType][currentForm]; +} + +/** + * Gets the previous form name. + */ +function getPrevForm() { + const forms = Object.keys(nextForms[sessionType]); + if (wizardType !== "create") forms.shift(); + return forms.filter(f => nextForms[sessionType][f] === currentForm)[0]; +} + +/** + * Gets all forms for the selected lab type. + */ +function getForms() { + let forms = []; + let current = "type"; + while (nextForms[sessionType][current] !== undefined) { + current = nextForms[sessionType][current]; + forms.push(current); + } + return forms; +} + +/** + * Shows the correct buttons depending on the location in the wizard. + */ +function showCorrectButtons() { + const cancel = $("[data-create-session-cancel]"); + const prev = $("[data-create-session-prev]"); + const next = $("[data-create-session-next]"); + const create = $("[data-create-session-create]"); + + const createButtonData = { + create: { label: "Create session", smallLabel: "Create", icon: "fa-plus" }, + copy: { label: "Copy session", smallLabel: "Copy", icon: "fa-copy" }, + edit: { label: "Edit session", smallLabel: "Edit", icon: "fa-pencil-alt" }, + }; + create.find("[data-role='label']").text(createButtonData[wizardType].label); + create.find("[data-role='small_label']").text(createButtonData[wizardType].smallLabel); + create + .find("[data-role='icon']") + .removeClass(...Object.keys(createButtonData).map(t => createButtonData[t].icon)) + .addClass(createButtonData[wizardType].icon); + + const nextForm = getNextForm(); + if (nextForm === undefined || wizardType !== "create") { + next.addClass("hidden"); + create.removeClass("hidden").prop( + "disabled", + getForms().some(f => !isValid(getForm(f))) + ); + } else { + next.removeClass("hidden").prop("disabled", !canGoToForm(nextForm)); + create.addClass("hidden"); + } + + if (getPrevForm() === undefined || wizardType !== "create") { + prev.addClass("hidden"); + cancel.removeClass("hidden"); + } else { + prev.removeClass("hidden"); + cancel.addClass("hidden"); + } + + const nav = $("#create-session-nav"); + if (currentForm === "type") nav.addClass("hidden"); + else nav.removeClass("hidden"); +} + +/** + * Goes to the specified form name. + */ +function goToForm(form) { + $(`[data-create-session-part="${currentForm}"]`).addClass("hidden"); + getForm(form).trigger("show").removeClass("hidden"); + + currentForm = form; + + showCorrectButtons(); + $("#create-session-part").text(getSentence(currentForm)); +} + +/** + * Creates the navigation element. + */ +function createNav() { + const nav = $("#create-session-nav"); + nav.find("*").remove(); + getForms().forEach(f => { + const button = $(`<button data-form="${f}">${getSentence(f)}</button>`); + const form = getForm(f); + button.attr( + "data-valid", + form.find(":where(input, select, textarea)").length > 0 && isValid(form) + ); + if (!canGoToForm(f)) button.prop("disabled", true); + nav.append(button); + button.click(() => { + if (canGoToForm(f)) goToForm(f); + }); + }); +} + +/** + * Clears all data from the forms. + */ +function clearForms() { + getForms().forEach(f => { + const form = getForm(f); + form.find("[data-remove-on-cancel]").remove(); + form.find(":where(input, select, textarea)").each((i, elem) => { + $(elem).val($(elem).data("initial")).change(); + $(elem).removeAttr("data-show-valid"); + if ($(elem).is("select")) updateSelect($(elem)); + }); + form.trigger("clear"); + }); + wizardType = "create"; + $("[data-create-session-wizard]").removeClass("hidden"); + goToForm("type"); +} + +/** + * Sets an input element to check for necessary validity of forms when it is changed. + */ +function setValidityCheck(input) { + input.change(function () { + const form = $(this).closest("form"); + $(`[data-form=${form.data("create-session-part")}]`).attr("data-valid", isValid(form)); + const next = getNextForm(); + if (next !== undefined) { + $("[data-create-session-next]").prop("disabled", !canGoToForm(next)); + } + $("[data-create-session-create]").prop( + "disabled", + getForms().some(f => !isValid(getForm(f))) + ); + $("[data-create-session-part]").each((i, f) => { + const name = $(f).data("create-session-part"); + $(`[data-form=${name}]`).prop("disabled", !canGoToForm(name)); + }); + }); +} + +/** + * Collects all data and submits it to the server. + * + * @param organisationalUnit The id of the edition / edition collection + */ +function submitSession(organisationalUnit, shared) { + let data = { organisationalUnit: organisationalUnit, id: $("#create-session-id").val() }; + + // Collect all values in data + getForms().forEach(f => + getForm(f) + .find(":where(input, select, textarea)") + .each((i, e) => { + const name = e.name; + const value = $(e).val(); + if (value === "") return; + data[name] = value; + }) + ); + + // Names with a ':' get added as a key-value pair in a map + Object.keys(data) + .filter(k => k.includes(":")) + .forEach(name => { + const key = name.split(":")[0]; + const newName = name.split(":")[1]; + if (data[newName] === undefined) data[newName] = {}; + data[newName][key] = data[name]; + delete data[name]; + }); + // Names with a '-' get collected to a list + Object.keys(data) + .filter(k => k.includes("-")) + .forEach(name => { + const newName = name.split("-")[0]; + if (data[newName] === undefined) data[newName] = []; + data[newName].push(...data[name]); + delete data[name]; + }); + + if (wizardType === "edit") { + data["_method"] = "patch"; + } + + $.ajax({ + url: + shared && wizardType === "create" + ? `/shared-edition/lab/${sessionType.toLowerCase()}` + : `/lab/${sessionType.toLowerCase()}`, + type: "post", + data: data, + success: () => window.location.reload(), + error: () => alert("Error creating session"), + }); +} diff --git a/src/main/resources/templates/assignment/create.html b/src/main/resources/templates/assignment/create.html index eae1b1452609bd975860036fd8259dffad4cdd27..8bef4ecd3b9f079be8c70bada37956fdf644b152 100644 --- a/src/main/resources/templates/assignment/create.html +++ b/src/main/resources/templates/assignment/create.html @@ -1,102 +1,78 @@ -<!-- - - 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}"> -<head> - <title th:text="|Create Assignment for ${'#' + edition.id}|"></title> - - <script src="/webjars/momentjs/min/moment.min.js"></script> - - <script type="application/javascript" - src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script> - <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/> - - <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script> - <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/> -</head> - -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> - -<!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleSummaryDTO"--> - -<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueAssignmentCreateDTO"--> - -<body> -<section layout:fragment="subcontent"> - <div class="page-header"> - <h3>Create Assignment</h3> - </div> - - <form th:with="action = @{/module/{mId}/assignment/create(id=${edition.id}, mId=${_module.id})}" - th:action="${action}" th:object="${dto}" - class="form-horizontal" method="post"> - <input type="hidden" name="moduleId" th:value="${_module.id}"> - - <div class="form-group form-row"> - <label for="name-input" class="col-sm-2 col-form-label">Name:</label> - <div class="col-sm-4"> - <input id="name-input" - type="text" class="form-control" - required th:field="*{name}" - placeholder="The name of the assignment"/> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="create-assignment" layout:fragment="dialog"> + <form th:action="@{/assignment}" method="post" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Create assignment</h2> + </div> + <button class="hidden:from_small">Create</button> </div> - </div> - <div class="form-group form-row"> - <label for="description-input" class="col-sm-2 col-form-label">Description:</label> - <div class="col-sm-10"> - <input id="description-input" - type="text" class="form-control" - th:field="*{description}" - placeholder="(Optionally) a description of the assignment"/> - </div> - </div> + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <input name="module" type="hidden" /> + + <div class="flex_column gap-1"> + <label class="form__label" for="create-assignment-name">Name</label> + <input + class="textfield" + id="create-assignment-name" + name="name" + placeholder="Assignment name" + data-style="outlined" + required /> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="create-assignment-description"> + Description (optional) + </label> + <textarea + class="textfield" + id="create-assignment-description" + name="description" + placeholder="Assignment description" + rows="3" + data-style="outlined" + style="resize: none"></textarea> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="create-assignment-deadline"> + Deadline (optional) + </label> + <input + class="textfield" + id="create-assignment-deadline" + type="datetime-local" + name="deadline" + data-style="outlined" /> + </div> + </div> - <div class="form-group form-row"> - <label for="deadline-input" class="col-sm-2 col-form-label">Deadline:</label> - <div class="col-sm-10 input-group" id="deadline-input" data-target-input="nearest"> - <input type="text" th:field="*{deadline}" - class="form-control datetimepicker-input" - data-target="#deadline-input"/> - <div class="input-group-append"> - <span class="input-group-text" - data-target="#deadline-input" - data-toggle="datetimepicker"> - <i class="fa fa-calendar"></i> - </span> + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create assignment</span> + </button> </div> </div> - </div> - - <div class="form-group"> - <button type="submit" class="btn btn-primary ctrl-enter-submit float-right"> - Create new Assignment - </button> - <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> - </div> - </form> - - <script type="application/javascript"> - $("#deadline-input").datetimepicker({format: "DD-MM-YYYY HH:mm"}) - </script> -</section> -</body> + </form> + </dialog> </html> diff --git a/src/main/resources/templates/assignment/edit.html b/src/main/resources/templates/assignment/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..bd1c76ddac8c4402aa6ed520867fa6952f0f6ba0 --- /dev/null +++ b/src/main/resources/templates/assignment/edit.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="edit-assignment" layout:fragment="dialog"> + <form th:action="@{/assignment}" th:method="patch" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Edit assignment</h2> + </div> + <button class="hidden:from_small">Create</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <input name="assignment" type="hidden" /> + + <div class="flex_column gap-1"> + <label class="form__label" for="edit-assignment-name">Name</label> + <input + class="textfield" + id="edit-assignment-name" + name="name" + placeholder="Assignment name" + data-style="outlined" + required /> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="edit-assignment-description"> + Description (optional) + </label> + <textarea + class="textfield" + id="edit-assignment-description" + name="description" + placeholder="Assignment description" + rows="3" + data-style="outlined" + style="resize: none"></textarea> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="edit-assignment-deadline"> + Deadline (optional) + </label> + <input + class="textfield" + id="edit-assignment-deadline" + type="datetime-local" + name="deadline" + data-style="outlined" /> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit assignment</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/assignment/remove.html b/src/main/resources/templates/assignment/remove.html index e08c73676030e1508252021595325097b134647e..6840c591aa12eed72540b8d5022eb938b7ee7d00 100644 --- a/src/main/resources/templates/assignment/remove.html +++ b/src/main/resources/templates/assignment/remove.html @@ -1,47 +1,50 @@ -<!-- - - 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="assignment" type="nl.tudelft.labracore.api.dto.AssignmentDetailsDTO"--> - -<body> -<section layout:fragment="subcontent"> - <div class="page-sub-header"> - <h3>Remove Assignment</h3> - </div> - - <form th:action="@{/assignment/{id}/remove(id=${assignment.id})}" - class="form-horizontal" - method="post"> - Are you sure you want to remove <strong - th:text="${'assignment #' + assignment.id + ' (' + assignment.name + ')'}"></strong>? - <div class="text-center"> - <button class="btn btn-danger">Delete this Assignment</button> - <small>or <a - th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small> - </div> - </form> -</section> -</body> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="remove-assignment" layout:fragment="dialog"> + <form th:action="@{/assignment}" th:method="delete" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Delete assignment</h2> + </div> + <button class="hidden:from_small">Delete</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-6"> + <input name="id" type="hidden" /> + + <p class="font-400"> + Are you sure you want to delete + <span id="remove-assignment-name"></span> + ? + </p> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + data-type="negative"> + <span class="margin_right-1 fa-solid fa-trash-alt"></span> + <span>Delete assignment</span> + </button> + </div> + </div> + </form> + </dialog> </html> diff --git a/src/main/resources/templates/container.html b/src/main/resources/templates/container.html new file mode 100644 index 0000000000000000000000000000000000000000..c996c10b3b9245fdd24db5dbe106f77a38f1cfa7 --- /dev/null +++ b/src/main/resources/templates/container.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{layout}"> + <body> + <div class="page" layout:fragment="container"> + <a href="#main-content" class="link jump_to_content">Skip to main content</a> + <th:block layout:replace="~{header :: header}"></th:block> + <main class="main" id="main-content"> + <div layout:fragment="breadcrumbs" class="breadcrumbs"></div> + <th:block layout:fragment="content">Content goes here.</th:block> + </main> + <th:block layout:replace="~{footer :: footer}"></th:block> + <th:block layout:fragment="overlays"></th:block> + <div id="toasts"></div> + </div> + </body> + + <th:block layout:fragment="scripts"> + <th:block layout:replace="~{header :: header_script}"></th:block> + <th:block layout:fragment="script"></th:block> + </th:block> +</html> diff --git a/src/main/resources/templates/course/request.html b/src/main/resources/templates/course/request.html index 09b23795143423f8b2910104234838c019a20254..c86bce98ea61fa317b16f9eab855c060fce6891c 100644 --- a/src/main/resources/templates/course/request.html +++ b/src/main/resources/templates/course/request.html @@ -1,86 +1,76 @@ -<!-- - - 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="~{layout}"> -<head> - <title>Request Course</title> -</head> - -<!--@thymesVar id="editions" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"--> - -<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.CourseRequestCreateDTO"--> - -<body> -<section layout:fragment="content"> - <nav role="navigation" class="breadcrumbs"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li> - <li class="breadcrumb-item"><a th:href="@{/editions}">Courses</a></li> - <li class="breadcrumb-item active" aria-current="page">Request Course</li> - </ol> - </nav> - - <form th:action="@{/editions/request-course}" - th:object="${dto}" - class="form-horizontal" method="post"> - <div class="form-group form-row"> - <label for="name-input" class="col-sm-2 col-form-label">Name:</label> - <div class="col-sm-10"> - <input id="name-input" - type="text" class="form-control" - required th:field="*{name}" - placeholder="The full name of the course"/> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="request-course" layout:fragment="dialog"> + <form th:action="@{/editions/request-course}" method="post" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Request course</h2> + </div> + <button class="hidden:from_small">Send</button> </div> - </div> - <div class="form-group form-row"> - <label for="code-input" class="col-sm-2 col-form-label">Code:</label> - <div class="col-sm-10"> - <input id="code-input" - type="text" class="form-control" - required th:field="*{code}" - placeholder="The course code (i.e. IT1100)"/> - </div> - </div> + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <div class="flex_column gap-1"> + <label for="course-name" class="form__label">Name</label> + <input + class="textfield" + id="course-name" + name="name" + placeholder="Course name" + data-style="outlined" + required /> + </div> - <div class="form-group form-row"> - <label for="remarks-input" class="col-sm-2 col-form-label">Remarks:</label> - <div class="col-sm-10"> - <input id="remarks-input" - type="text" class="form-control" - th:field="*{remarks}" - placeholder="Any remarks you want to give regarding your course."/> - </div> - </div> + <div class="flex_column gap-1"> + <label class="form__label" for="course-code">Course code</label> + <input + class="textfield" + id="course-code" + name="code" + placeholder="Course code" + data-style="outlined" + required /> + </div> - <div class="form-group form-row"> - <div class="col-sm-offset-2 col-sm-8"> - <button type="submit" class="btn btn-primary ctrl-enter-submit"> - Request new Course - </button> - <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> - </div> - </div> - </form> -</section> -</body> + <div class="flex_column gap-1"> + <label class="form__label" for="course-remarks">Remarks</label> + <textarea + class="textfield" + id="course-remarks" + name="remarks" + placeholder="Any remarks regarding your course" + data-style="outlined" + rows="3" + style="resize: none"></textarea> + </div> + </div> + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + type="submit"> + <span class="margin_right-1 fa-solid fa-paper-plane"></span> + <span>Send request</span> + </button> + </div> + </div> + </form> + </dialog> </html> diff --git a/src/main/resources/templates/edition/create.html b/src/main/resources/templates/edition/create.html index a83ee67616caaec00a20d30407d93e7fe68aad9c..631d0a60ce38ae69dd105c71b79f48a4c7123e0b 100644 --- a/src/main/resources/templates/edition/create.html +++ b/src/main/resources/templates/edition/create.html @@ -1,165 +1,168 @@ -<!-- - - 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="~{admin/view}"> - -<!--@thymesVar id="edition" type="nl.tudelft.queue.dto.create.QueueEditionCreateDTO"--> - -<!--@thymesVar id="cohorts" type="java.util.List<nl.tudelft.labracore.api.dto.CohortSummaryDTO>"--> -<!--@thymesVar id="courses" type="java.util.List<nl.tudelft.labracore.api.dto.CourseSummaryDTO>"--> - -<head> - <script src="/webjars/momentjs/min/moment.min.js"></script> - - <script type="application/javascript" - src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script> - <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/> - - <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script> - <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/> -</head> - -<body> -<section layout:fragment="subcontent"> - <div class="page-sub-header"> - <h3>Create edition</h3> - </div> - - <form th:action="@{/edition/add}" - class="form-horizontal" - th:object="${edition}" - method="post" - enctype="multipart/form-data"> - <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> - - <div class="form-group text-danger" - th:if="${#lists.isEmpty(courses)}"> - You are not the manager of any course, please - <a th:href="@{/editions/request-course}"> - request a course first - </a>. - </div> - - <div class="form-group" - th:unless="${#lists.isEmpty(courses)}"> - Select the course under which you want to create a course edition. If your course is not in the list, or has - a different code, please - <a th:href="@{/editions/request-course}"> - request a course first - </a>. - </div> - - <div class="form-group"> - <label for="course-select">Select the course:</label> - <select th:field="*{course}" id="course-select" class="selectpicker col-md-6"> - <option value="none" disabled selected>Select course</option> - <option th:each="course : ${courses}" th:value="${course.id}" th:text="|${course.name} (${course.code})|"></option> - </select> - </div> - - <div class="form-group"> - <label for="edition-name">Edition name: (please name this after the year/semester, so for instance Q1 20/21, - this will be displayed in the course list as "[COURSE NAME] (Q1 20/21)"</label> - <input th:field="*{name}" type="text" id="edition-name" - placeholder="Edition name" class="form-control"/> - </div> - - <div class="form-group"> - Pick a cohort that you will teach during this edition. If you cannot find a fitting cohort, please select - one of the available cohorts. This will not impact any functionality for a standard course edition. - </div> - - <div class="form-group"> - <label for="cohort-select">Select the cohort:</label> - <select th:field="*{cohort}" id="cohort-select" class="selectpicker"> - <option value="none" disabled selected>Select cohort</option> - <option th:each="cohort : ${cohorts}" th:value="${cohort.id}" th:text="${cohort.name}"></option> - </select> - </div> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="create-edition" layout:fragment="dialog"> + <form th:action="@{/edition}" method="post" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Create course edition</h2> + </div> + <button class="hidden:from_small">Create</button> + </div> - <div class="form-group form-row"> - <div id="date-input" class="col-sm-10 row"> - <div class="col-sm-6 input-group" id="opens-at-input" data-target-input="nearest"> - <div class="input-group-prepend"> - <span class="input-group-text">From</span> - </div> - <input th:classappend="${#fields.hasErrors('startDate')} ? 'is-invalid'" - type="text" th:field="*{startDate}" - class="form-control datetimepicker-input" - data-target="#opens-at-input" required/> - <div class="input-group-append"> - <span class="input-group-text" - data-target="#opens-at-input" - data-toggle="datetimepicker"> - <i class="fa fa-calendar"></i> - </span> + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <div class="flex_column gap-1"> + <div class="flex gap-2 align_items_center"> + <label for="edition-name" class="form__label">Name</label> + <div class="tooltip"> + <button + type="button" + class="fa-solid fa-info-circle font-400 margin_bottom-3"></button> + <p class="font-300" style="min-width: min(75ch, 70vw)"> + Name this after the year/semester/quarter, e.g. 2022/2023 Q1, + this will be displayed as "Course name - 2022/2023 Q1". + </p> + </div> + </div> + <input + class="textfield" + id="edition-name" + name="name" + placeholder="Edition name" + data-style="outlined" + required /> </div> - <div class="invalid-feedback" th:if="${#fields.hasErrors('startDate')}" - th:errors="*{startDate}">Slot opensAt error + + <div class="flex_column gap-1"> + <label class="form__label" for="edition-course">Course</label> + <select + id="edition-course" + name="course" + data-select + data-style="outlined" + required + data-placeholder="Select course"> + <option data-empty-option value=""></option> + <option + th:each="course : ${courses}" + th:value="${course.id}" + th:text="${course.name}"></option> + </select> + <p class="font-300"> + Don't see your course? Click + <button + type="button" + class="link" + data-dialog="request-course" + data-close-current> + here + </button> + to request one. + </p> </div> - </div> - <div class="col-sm-6 input-group date" id="closes-at-input" data-target-input="nearest"> - <div class="input-group-prepend"> - <span class="input-group-text">To</span> + <div class="flex_column gap-1"> + <div class="flex gap-2 align_items_center"> + <label for="edition-cohort" class="form__label">Cohort</label> + <div class="tooltip"> + <button + type="button" + class="fa-solid fa-info-circle font-400 margin_bottom-3"></button> + <p class="font-300" style="min-width: min(80ch, 70vw)"> + Pick a cohort that you will teach during this edition. If you + cannot find a fitting cohort, please select one of the available + cohorts. This will not impact any functionality for a standard + course edition. + </p> + </div> + </div> + <select + name="cohort" + id="edition-cohort" + data-select + data-style="outlined" + required + data-placeholder="Select cohort"> + <option data-empty-option value=""></option> + <option + th:each="cohort : ${cohorts}" + th:value="${cohort.id}" + th:text="${cohort.name}"></option> + </select> </div> - <input th:classappend="${#fields.hasErrors('endDate')} ? 'is-invalid'" - type="text" th:field="*{endDate}" - class="form-control datetimepicker-input" - data-target="#closes-at-input" required/> - <div class="input-group-append"> - <span class="input-group-text" - data-target="#closes-at-input" - data-toggle="datetimepicker"> - <i class="fa fa-calendar"></i> - </span> + + <div class="flex_column gap-1"> + <div class="flex gap-2 align_items_center"> + <label for="edition-start" class="form__label">Start date</label> + <div class="tooltip"> + <button + type="button" + class="fa-solid fa-info-circle font-400 margin_bottom-3"></button> + <p class="font-300" style="min-width: min(80ch, 70vw)"> + This is the time from which the course edition will be visible. + It is recommended setting this time a bit before the the start + of your course. + </p> + </div> + </div> + <input + class="textfield" + type="datetime-local" + id="edition-start" + name="startDate" + data-style="outlined" + required /> </div> - <div class="invalid-feedback" th:if="${#fields.hasErrors('endDate')}" - th:errors="*{endDate}">Slot error + + <div class="flex_column gap-1"> + <div class="flex gap-2 align_items_center"> + <label for="edition-end" class="form__label">End date</label> + <div class="tooltip"> + <button + type="button" + class="fa-solid fa-info-circle font-400 margin_bottom-3"></button> + <p class="font-300" style="min-width: min(80ch, 70vw)"> + This is the time until which the course edition will be visible. + It is recommended setting this time a bit after the last resit + for your course. + </p> + </div> + </div> + <input + class="textfield" + type="datetime-local" + id="edition-end" + name="endDate" + data-style="outlined" + required /> </div> </div> - </div> - </div> - <div class="form-group"> - <div class="float-right"> - <input type="submit" class="btn btn-primary" value="Create edition"/> + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + type="submit"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create edition</span> + </button> + </div> </div> - </div> - </form> - - <!-- Invisible column for spacing at the bottom of the page --> - <div class="row mt-3"></div> - <script th:inline="javascript" type="text/javascript"> - //<![CDATA[ - - $(function () { - const opensAtInput = $("#opens-at-input"); - const closesAtInput = $("#closes-at-input"); - const timePickerInputs = [opensAtInput, closesAtInput]; - - timePickerInputs.forEach(input => input.datetimepicker({format: "DD-MM-YYYY HH:mm"})); - }) - //]]> - </script> -</section> -</body> + </form> + </dialog> </html> diff --git a/src/main/resources/templates/edition/edit.html b/src/main/resources/templates/edition/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..32d59dabb1f0c4ef22f265bddce94dc1b238a410 --- /dev/null +++ b/src/main/resources/templates/edition/edit.html @@ -0,0 +1,159 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="edit-edition" layout:fragment="dialog"> + <form th:action="@{/edition}" th:method="patch" class="dialog"> + <input type="hidden" name="edition" th:value="${edition.id}" /> + + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Edit course edition</h2> + </div> + <button class="hidden:from_small">Edit</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <div class="flex_column gap-1"> + <div class="flex gap-2 align_items_center"> + <label for="edition-name" class="form__label">Name</label> + <div class="tooltip"> + <button + type="button" + class="fa-solid fa-info-circle font-400 margin_bottom-3"></button> + <p class="font-300" style="min-width: min(75ch, 70vw)"> + Name this after the year/semester/quarter, e.g. 2022/2023 Q1, + this will be displayed as "Course name - 2022/2023 Q1". + </p> + </div> + </div> + <input + class="textfield" + id="edition-name" + name="name" + placeholder="Edition name" + data-style="outlined" + required + th:value="${edition.name}" /> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="edition-course">Course</label> + <select + id="edition-course" + name="course" + data-select + data-style="outlined" + required + data-placeholder="Select course"> + <option + th:each="course : ${courses}" + th:value="${course.id}" + th:text="${course.name}" + th:selected="${edition.course.id == course.id}"></option> + </select> + </div> + + <div class="flex_column gap-1"> + <div class="flex gap-2 align_items_center"> + <label for="edition-cohort" class="form__label">Cohort</label> + <div class="tooltip"> + <button + type="button" + class="fa-solid fa-info-circle font-400 margin_bottom-3"></button> + <p class="font-300" style="min-width: min(80ch, 70vw)"> + Pick a cohort that you will teach during this edition. If you + cannot find a fitting cohort, please select one of the available + cohorts. This will not impact any functionality for a standard + course edition. + </p> + </div> + </div> + <select + name="cohort" + id="edition-cohort" + data-select + data-style="outlined" + required + data-placeholder="Select cohort"> + <option + th:each="cohort : ${cohorts}" + th:value="${cohort.id}" + th:text="${cohort.name}" + th:selected="${edition.cohort.id == cohort.id}"></option> + </select> + </div> + + <div class="flex_column gap-1"> + <div class="flex gap-2 align_items_center"> + <label for="edition-start" class="form__label">Start date</label> + <div class="tooltip"> + <button + type="button" + class="fa-solid fa-info-circle font-400 margin_bottom-3"></button> + <p class="font-300" style="min-width: min(80ch, 70vw)"> + This is the time from which the course edition will be visible. + It is recommended setting this time a bit before the the start + of your course. + </p> + </div> + </div> + <input + class="textfield" + type="datetime-local" + id="edition-start" + name="startDate" + data-style="outlined" + th:value="${#temporals.format(edition.startDate, "yyyy-MM-dd'T'HH:mm")}" + required /> + </div> + + <div class="flex_column gap-1"> + <div class="flex gap-2 align_items_center"> + <label for="edition-end" class="form__label">End date</label> + <div class="tooltip"> + <button + type="button" + class="fa-solid fa-info-circle font-400 margin_bottom-3"></button> + <p class="font-300" style="min-width: min(80ch, 70vw)"> + This is the time until which the course edition will be visible. + It is recommended setting this time a bit after the last resit + for your course. + </p> + </div> + </div> + <input + class="textfield" + type="datetime-local" + id="edition-end" + name="endDate" + data-style="outlined" + th:value="${#temporals.format(edition.endDate, "yyyy-MM-dd'T'HH:mm")}" + required /> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit edition</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/edition/enrol.html b/src/main/resources/templates/edition/enrol.html index 7029e5f84a9a5955f715b1371334ab6a7ac6f383..803dfc345ad94deb33533dfbd1d6bedbc2b81c90 100644 --- a/src/main/resources/templates/edition/enrol.html +++ b/src/main/resources/templates/edition/enrol.html @@ -1,50 +1,47 @@ -<!-- - - 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="~{layout}"> - -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> - -<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"><a href="/editions">Courses</a></li> - <li class="breadcrumb-item"><a href="#" th:href="@{/edition/{id}/enrol(id=${edition.id})}" th:text="${edition.course.id}"></a></li> - <li class="breadcrumb-item active">Enrol</li> - </ol> - </nav> - - <div class="page-header"> - <h1>Enrol</h1> - </div> - - <p>Do you wish to enrol for the course edition <strong th:text="${edition.course.name}"></strong>?</p> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="enrol" layout:fragment="dialog"> + <form th:action="@{/edition/enrol}" method="post" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Enrol for edition</h2> + </div> + <button class="hidden:from_small">Enrol</button> + </div> - <form th:action="@{/edition/{id}/enrol(id=${edition.id})}" class="form-horizontal" method="post"> - <div class="text-center"> - <button type="submit" name="enrol" class="btn btn-success">Enrol</button> - <small> or <a th:href="@{/editions}">go back</a></small> + <div class="dialog__body"> + <div class="flex_column margin_bottom-6"> + <input name="editionId" type="hidden" /> + + <p class="font-400"> + Are you sure you want to enrol for + <span id="enrol-edition-name"></span> + ? + </p> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Enrol</span> + </button> + </div> </div> </form> - </section> -</body> + </dialog> </html> diff --git a/src/main/resources/templates/edition/index.html b/src/main/resources/templates/edition/index.html index 7f2200039562cd3bd7e8d8571c91695e941a0261..1d3671e7c0498316499675ced9cee528196ce5c9 100644 --- a/src/main/resources/templates/edition/index.html +++ b/src/main/resources/templates/edition/index.html @@ -1,166 +1,105 @@ -<!-- - - 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="~{layout}"> -<head> - <title>Editions</title> - - <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"/> -</head> - -<!--@thymesVar id="editions" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"--> - -<!--@thymesVar id="programs" type="java.util.List<nl.tudelft.labracore.api.dto.ProgramSummaryDTO>"--> - -<!--@thymesVar id="filter" type="nl.tudelft.queue.dto.util.EditionFilterDTO"--> - -<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 active" aria-current="page">Courses</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> - </th:block> - <h1>Course Editions</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"> - <select multiple class="selectpicker" size="10" - id="program-select" name="programs" - data-mobile="true" data-width="100%" - data-selected-text-format="count > 1"> - <th:block th:each="program : ${programs}"> - <option th:value="${program.id}" - th:selected="${(filter?.programs ?: {}).contains(program.id)}" - th:text="${program.name}"> - </option> - </th:block> - </select> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <head> + <title>Catalogue</title> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <th:block layout:fragment="content"> + <h1 class="title margin_bottom-5">Course catalogue</h1> + + <form + id="filters" + class="flex_column gap-3 align_items_start margin_bottom-5" + th:action="@{/editions/filter}" + method="post"> + <div class="searchbox" data-style="floating"> + <input + aria-label="Course search" + type="text" + name="nameSearch" + placeholder="Search" + th:value="${filter.nameSearch}" /> + <button class="fa-solid fa-search"></button> </div> - </div> - - <div class="form-group"> - <input class="form-control" name="nameSearch" th:value="${filter?.nameSearch ?: ''}"> - </div> - - <div class="form-group"> - <div class="col-sm-2"> - <button class="btn btn-primary" type="submit">Filter</button> - </div> - </div> - </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"> - <thead> - <tr> - <th>Edition</th> - <th>Code</th> - <th>Teacher</th> - <th></th> - </tr> - </thead> - <tbody> - <tr th:each="edition : ${editions}"> - <td><a 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"> - <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:block> - <th:block th:if="${@permissionService.canViewEdition(edition)}"> - <a class="btn btn-primary" - th:href="@{/edition/{id}(id=${edition.id})}">view</a> - </th:block> - </td> - </tr> - </tbody> - </table> - </div> - - <!-- Shown on small devices (phone/tablet) --> - <div class="d-block d-md-none"> - <div th:each="edition : ${editions}" class="card" - th:classappend="${@permissionService.canViewEdition(edition)} ? 'bg-primary text-white' : 'bg-light'" - style="margin-bottom:20px;"> - <h4 class="card-header text-dark" th:text="|${edition.course.name} (${edition.name})|"></h4> - - <div class="card-body"> - <dl> - <dt class="text-dark">Teacher</dt> - <dd class="truncate text-dark" - th:text="${#strings.listJoin(@roleDTOService.teacherNames(edition), ', ')}"></dd> - </dl> - - </div> - <div class="card-footer"> - <div class="btn-group btn-group-justified"> - <th:block th:if="${@permissionService.canViewEdition(edition)}"> - <a th:href="@{/edition/{id}(id=${edition.id})}" - class="btn btn-primary btn-sm">View</a> - </th:block> - - <th:block th:unless="${@permissionService.canViewEdition(edition)}"> - <a th:href="@{/edition/{id}/enrol(id=${edition.id})}" - class="btn btn-success btn-sm">Enrol</a> - </th:block> - </div> - </div> - </div> - </div> - - <nav th:include="pagination :: pagination (page=${editions}, size=3)"></nav> -</section> -</body> - + <select + class="hidden" + aria-label="Program" + name="programs" + data-select + multiple + data-placeholder="Programs" + data-style="floating"> + <option + th:each="program : ${programs}" + th:value="${program.id}" + th:text="${program.name}" + th:selected="${filter.programs?.contains(program.id)} ?: false"></option> + </select> + </form> + + <th:block layout:replace="~{edition/table :: table}"></th:block> + </th:block> + + <th:block layout:fragment="overlays"> + <th:block layout:replace="~{edition/enrol :: dialog}"></th:block> + </th:block> + + <script layout:fragment="script"> + /** + * Replaces the current table with the new filtered table + */ + function getFilteredEditions() { + $("#filters").submit(); + const pagination = $(".pagination"); + $.get( + `/editions/list?page=${pagination.data("page")}&size=${pagination.data( + "page-size" + )}`, + html => { + const element = $(html); + $("#editions").replaceWith(element); + } + ); + } + + $(() => { + // Filter on change of programs + $("select[name='programs']").change(function () { + getFilteredEditions(); + }); + // Filter when user stopped typing + let timeout = null; + $("input[name='nameSearch']").keydown(function () { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + getFilteredEditions(); + }, 250); + }); + // Do not reload page when submitting the filters + $("#filters").submit(function (event) { + event.preventDefault(); + $.ajax({ + url: "/editions/filter", + type: "post", + data: { + nameSearch: $("input[name='nameSearch']").val(), + programs: $("select[name='programs']").val().join(","), + }, + success: () => {}, + }); + }); + + // Enrol overlay set id + $("[data-dialog='enrol']").click(function () { + $("#enrol input[name='editionId']").val($(this).data("edition")); + $("#enrol-edition-name").text($(this).closest("tr").find("a").text()); + }); + }); + </script> </html> diff --git a/src/main/resources/templates/edition/table.html b/src/main/resources/templates/edition/table.html new file mode 100644 index 0000000000000000000000000000000000000000..ef8ace8f002dee5749a550001d0fcb6b5a47ce01 --- /dev/null +++ b/src/main/resources/templates/edition/table.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div id="editions" class="flex_column gap-5" layout:fragment="table"> + <table layout:fragment="table" class="shadow rounded table table__to_cards"> + <thead> + <tr class="heading"> + <th>Course</th> + <th>Code</th> + <th>Teachers</th> + <th></th> + </tr> + </thead> + <tbody> + <tr th:if="${editions.isEmpty()}"> + <td>No courses found</td> + <td></td> + <td></td> + <td></td> + </tr> + <tr th:each="edition : ${editions}"> + <td> + <a + class="link" + 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="table__actions"> + <button + class="padding_inline-5 padding_block-1 button" + data-style="outlined" + data-dialog="enrol" + th:data-edition="${edition.id}" + th:disabled="${@permissionService.canViewEdition(edition) or edition.isArchived}"> + <span class="fa-solid fa-plus margin_right-1"></span> + <span>Enrol</span> + </button> + </td> + </tr> + </tbody> + </table> + + <th:block layout:replace="~{pagination :: pagination(page=${editions}, size=3)}"></th:block> + </div> +</html> diff --git a/src/main/resources/templates/edition/view.html b/src/main/resources/templates/edition/view.html index 67309819aa9cd76ff3fa7f836b0b421ee089c3ac..c83dddc0cf76285d540f92791e977abaa5c66d45 100644 --- a/src/main/resources/templates/edition/view.html +++ b/src/main/resources/templates/edition/view.html @@ -1,107 +1,140 @@ -<!-- - - 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="~{layout}"> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <head> + <title th:text="|${edition.course.name} - ${edition.name}|"></title> + </head> -<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"--> -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + <div layout:fragment="breadcrumbs" class="breadcrumbs"> + <a th:href="@{/}">My courses</a> + <span>></span> + <a + th:text="|${edition.course.name} - ${edition.name}|" + th:href="@{/edition/{id}(id=${edition.id})}"></a> + </div> -<!--@thymesVar id="message" type="java.lang.String"--> + <th:block layout:fragment="content"> + <div + class="flex space_between align_items_center margin_bottom-3:from_small | flex_column:small align_items_stretch:small"> + <h1 class="title" th:text="|${edition.course.name} - ${edition.name}|"></h1> + <div class="flex gap-3 | flex_column:small margin_bottom-5:small gap-5:small"> + <button + th:if="${@permissionService.canManageEdition(edition.id)}" + class="button padding_block-3 padding_inline-5" + data-style="floating" + data-dialog="edit-edition"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit course edition</span> + </button> + <form + th:if="${@permissionService.canEnrolForEdition(edition.id)}" + th:action="@{/edition/enrol}" + method="post" + class="flex_column:small"> + <input name="editionId" type="hidden" th:value="${edition.id}" /> + <button class="button padding_block-3 padding_inline-5" data-style="floating"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Enrol</span> + </button> + </form> + <form + th:if="${@permissionService.canLeaveEdition(edition.id)}" + th:action="@{/edition/leave}" + method="post" + class="flex_column:small"> + <input name="editionId" type="hidden" th:value="${edition.id}" /> + <button + class="button padding_block-3 padding_inline-5" + data-type="negative" + data-style="floating"> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Leave edition</span> + </button> + </form> + </div> + </div> -<body> -<section layout:fragment="content"> - <section layout:fragment="breadcrumb"> - <nav role="navigation" class="breadcrumbs"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li> - <li class="breadcrumb-item"> - <a th:if="${edition != null && @permissionService.canEnrolForEdition(edition.id)}" - th:href="@{/editions}">Catalog</a> - <a th:unless="${edition != null && @permissionService.canEnrolForEdition(edition.id)}" - th:href="@{/}">My Courses</a> - </li> - <li class="breadcrumb-item active" th:if="${ec == null}" th:text="${edition.course.code}">TI1316</li> - <li class="breadcrumb-item active" th:unless="${ec == null}" th:text="${ec.name}">TI1316</li> - </ol> - </nav> - </section> + <div + th:if="${@permissionService.canManageModules(edition.id)} and ${edition.modules.isEmpty()}" + class="banner margin_bottom-3" + data-type="error"> + <span class="fa-solid fa-exclamation-circle"></span> + <p> + This edition does not have any modules. Without modules, it is not possible to + create sessions. + </p> + </div> + <div + th:if="${@permissionService.canManageAssignments(edition.id)} and ${not edition.modules.isEmpty() and assignments.isEmpty()}" + class="banner margin_bottom-3" + data-type="error"> + <span class="fa-solid fa-exclamation-circle"></span> + <p> + This edition does not have any assignments. Without assignments, it is not possible + to create sessions. + </p> + </div> + <div + th:if="${edition.startDate.isAfter(#temporals.createNow()) and @permissionService.canManageEdition(edition.id)}" + class="banner margin_bottom-3" + data-type="warning"> + <span class="fa-solid fa-exclamation-triangle"></span> + <p>Students cannot enrol for this edition, as it has not started yet.</p> + </div> + <div + th:if="${edition.endDate.isBefore(#temporals.createNow())}" + class="banner margin_bottom-3" + data-type="info"> + <span class="fa-solid fa-info-circle"></span> + <p>This edition is over.</p> + </div> - <ul class="nav nav-tabs" th:if="${ec == null}"> - <li class="nav-item"> - <a href="#" class="nav-link" - th:classappend="${#request.requestURI.matches('.*/edition/\d+') ? 'active' : ''}" - th:href="@{/edition/{id}(id=${edition.id})}"> - <i class="fa fa-th-large" aria-hidden="true"></i> Info - </a> - </li> - <li class="nav-item" th:if="${@permissionService.canManageParticipants(edition.id)}"> - <a href="#" class="nav-link" - th:classappend="${#request.requestURI.matches('.*/participants.*') ? 'active' : ''}" - th:href="@{/edition/{id}/participants(id=${edition.id})}"> - <i class="fa fa-user" aria-hidden="true"></i> Participants + <nav aria-label="Edition tabs" class="tabs margin_bottom-5"> + <a + th:data-active="${#request.requestURI.matches('.*/labs.*')}" + th:href="@{/edition/{id}/labs(id=${edition.id})}" + th:if="${@permissionService.canViewEdition(edition.id)} and not ${assignments.isEmpty()}"> + <span class="fa-solid fa-calendar-alt"></span> + <span>Sessions</span> </a> - </li> - <li class="nav-item" th:if="${@permissionService.canViewEdition(edition.id)}"> - <a href="#" class="nav-link" - th:classappend="${#request.requestURI.matches('.*/modules.*') ? 'active' : ''}" - th:href="@{/edition/{id}/modules(id=${edition.id})}"> - <i class="fa fa-boxes" aria-hidden="true"></i> Modules + <a + th:unless="${@permissionService.canViewEdition(edition.id)}" + th:data-active="${#request.requestURI.matches('.*/staff.*')}" + th:href="@{/edition/{id}/staff(id=${edition.id})}"> + <span class="fa-solid fa-user"></span> + <span>Staff</span> </a> - </li> - <li class="nav-item" th:if="${@permissionService.canViewEdition(edition.id)}"> - <a href="#" class="nav-link" - th:classappend="${#request.requestURI.matches('.*/labs.*') ? 'active' : ''}" - th:href="@{/edition/{id}/labs(id=${edition.id})}"> - <i class="fa fa-calendar" aria-hidden="true"></i> Labs + <a + th:if="${@permissionService.canManageParticipants(edition.id)}" + th:data-active="${#request.requestURI.matches('.*/participants.*')}" + th:href="@{/edition/{id}/participants(id=${edition.id})}"> + <span class="fa-solid fa-user"></span> + <span>Participants</span> </a> - </li> - <li class="nav-item" th:if="${@permissionService.canManageAssignments(edition.id)}"> - <a href="#" class="nav-link" - th:classappend="${#request.requestURI.matches('.*/status.*') ? 'active' : ''}" - th:href="@{/edition/{id}/status(id=${edition.id})}"> - <i class="fa fa-bar-chart" aria-hidden="true"></i> Status + <a + th:data-active="${#request.requestURI.matches('.*/modules.*')}" + th:href="@{/edition/{id}/modules(id=${edition.id})}" + th:if="${@permissionService.canViewEdition(edition.id)}"> + <span class="fa-solid fa-boxes"></span> + <span>Modules</span> </a> - </li> - <li class="nav-item" th:if="${@permissionService.canManageQuestions(edition.id)}"> - <a href="#" class="nav-link" - th:classappend="${#request.requestURI.matches('.*/questions.*') ? 'active' : ''}" - th:href="@{/edition/{id}/questions(id=${edition.id})}"> - <i class="fa fa-question" aria-hidden="true"></i> Questions + <a + th:if="${@permissionService.canManageQuestions(edition.id)}" + th:data-active="${#request.requestURI.matches('.*/questions.*')}" + th:href="@{/edition/{id}/questions(id=${edition.id})}"> + <span class="fa-solid fa-question"></span> + <span>Questions</span> </a> - </li> - </ul> + </nav> - <div class="alert alert-info mt-md-3" role="alert" th:unless="${#strings.isEmpty(message)}"> - <th:block th:text="${message}"> - </th:block> - <button type="button" class="close" data-dismiss="alert" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - </div> + <th:block layout:fragment="edition-content"></th:block> + </th:block> - <div class="pb-3"> - <div layout:fragment="subcontent"> - Sub content page - </div> - </div> -</section> -</body> + <th:block layout:fragment="overlays"> + <th:block layout:replace="~{edition/edit :: dialog}"></th:block> + <th:block layout:fragment="extra_overlays"></th:block> + </th:block> </html> diff --git a/src/main/resources/templates/edition/view/labs.html b/src/main/resources/templates/edition/view/labs.html index 30d50704ec5ca7269cbc51ef6e73ae08d34f1235..972fb54ab655393b3767fe1a7ca57fa31f03985c 100644 --- a/src/main/resources/templates/edition/view/labs.html +++ b/src/main/resources/templates/edition/view/labs.html @@ -1,157 +1,109 @@ -<!-- - - 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="labs" type="java.util.List<nl.tudelft.queue.dto.view.QueueSessionSummaryDTO>"--> -<!--@thymesVar id="allLabTypes" type="nl.tudelft.queue.models.enums.LabType[]"--> -<!--@thymesVar id="queueSessionTypes" type="java.util.List<nl.tudelft.queue.model.enums.QueueSessionType>"--> -<!--@thymesVar id="allModules" type="java.util.List<nl.tudelft.labracore.dto.view.structured.summary.ModuleSummaryDTO>"--> -<!--@thymesVar id="modules" type="java.util.List<Long>"--> - -<!--@thymesVar id="alert" type="java.lang.String"--> - -<head> - <title th:text="|Labs - ${edition.course.name} (${edition.name})|"></title> - - <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"/> -</head> - -<body> -<section layout:fragment="subcontent"> - - <div class="alert alert-danger mt-md-3" role="alert" th:unless="${#strings.isEmpty(alert)}" - th:text="${alert}"> - <button type="button" class="close" data-dismiss="alert" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - </div> - - <div class="page-sub-header"> - <div class="float-right" th:if="${@permissionService.canManageSessions(edition.id)}"> - <a th:each="lType : ${T(nl.tudelft.queue.model.enums.QueueSessionType).values()}" - href="#" th:href="@{/edition/{id}/lab/create(id=${edition.id}, type=${lType.name()})}" - th:text="|Create ${lType.displayName} Lab|" - class="btn btn-primary mr-1"> - </a> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{edition/view}"> + <th:block layout:fragment="edition-content"> + <div + class="flex space_between align_items_center margin_bottom-5 | flex_column:small align_items_stretch:small"> + <form id="filters" class="flex gap-3 | flex_column:small gap-5:small"> + <input type="hidden" name="edition" th:value="${edition.id}" /> + <select + name="queueSessionTypes" + class="hidden" + aria-label="Session type" + data-select + multiple + data-placeholder="Session type" + data-style="floating"> + <option + th:each="type : ${T(nl.tudelft.queue.model.enums.QueueSessionType).values()}" + th:value="${type}" + th:selected="${param.queueSessionTypes?.toString()?.contains(type.name())} ?: false" + th:text="#{|lab.type.${type.name.toLowerCase()}|}"></option> + </select> + <select + name="modules" + class="hidden" + aria-label="Module" + data-select + multiple + data-placeholder="Module" + data-style="floating"> + <option + th:each="module : ${allModules}" + th:value="${module.id}" + th:text="${module.name}" + th:selected="${param.modules?.contains(module.id.toString())} ?: false"></option> + </select> + </form> + <div th:if="${@permissionService.canManageSessions(edition.id)}" class="flex gap-3"> + <button + class="button padding_block-3 padding_inline-5 | flex_grow:small" + data-style="floating"> + <span class="margin_right-1 fa-solid fa-file-export"></span> + <span>Export sessions</span> + </button> + <button + class="button padding_block-3 padding_inline-5 | flex_grow:small" + data-style="floating" + data-dialog="create-session"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create session</span> + </button> + </div> </div> - <h3>Labs</h3> - </div> - <th:block th:unless="${#lists.isEmpty(labs) and #lists.isEmpty(queueSessionTypes) and #lists.isEmpty(modules)}"> - <form id="filter-form" method="get" class="form-inline mb-3" - th:action="@{/edition/{id}/labs(id=${edition.id})}"> - <div class="container-fluid"> - <div class="row align-items-end"> - <div class="form-group col-sm-2"> - <label class="form-control-label" for="lab-type-select">Type</label> - <select multiple class="form-control selectpicker" data-width="100%" th:name="queueSessionTypes" id="lab-type-select"> - <th:block th:each="labType : ${allLabTypes}"> - <option th:value="${labType}" - th:text="${labType.displayName}" - th:selected="${queueSessionTypes.contains(labType)}"> - </option> - </th:block> - </select> - </div> - <div class="form-group col-sm-2"> - <label class="form-control-label" for="module-select">Module</label> - <select multiple class="form-control selectpicker" data-width="100%" th:name="modules" id="module-select"> - <th:block th:each="module : ${allModules}"> - <option th:value="${module.id}" - th:text="${module.name}" - th:selected="${modules.contains(module.id)}"> - </option> - </th:block> - </select> - </div> - <div class="col-auto"> - <button type="submit" class="btn btn-primary"> - Filter - </button> - <a th:unless="${#lists.isEmpty(queueSessionTypes) and #lists.isEmpty(modules)}" - class="btn" - th:href="@{/edition/{id}/labs(id=${edition.id})}"> - <i class="fa fa-times" aria-hidden="true"></i> Clear current filters - </a> - </div> - </div> - </div> - </form> + <th:block th:with="canManage = ${@permissionService.canManageSessions(edition.id)}"> + <th:block layout:replace="~{edition/view/labs_tables :: tables}"></th:block> + </th:block> </th:block> - <th:block th:if="${#lists.isEmpty(labs)}"> - No labs for this edition. + <th:block layout:fragment="extra_overlays"> + <th:block layout:replace="~{lab/create :: dialog}"></th:block> + <th:block layout:replace="~{lab/remove :: dialog}"></th:block> </th:block> - <th:block th:unless="${#lists.isEmpty(labs)}"> - <ul class="list-group sort-open"> - <th:block th:each="lab : ${labs}"> - <li class="list-group-item lab-item" - th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'"> - <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" - th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> - </a> - - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.open()}">Active</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.closed()}">Completed</span> - - <span class="badge badge-pill badge-danger text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> - <span class="badge badge-pill badge-warning text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> - - <span class="badge badge-pill badge-warning text-muted" - th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> - - <div class="btn-group float-right"> - <th:block th:if="${@permissionService.canManageSessions(edition.id)}"> - <a th:href="@{/lab/{id}/edit(id=${lab.id})}" class="btn btn-sm btn-secondary"> - <i class="fa fa-pencil" aria-hidden="true"></i> - </a> - </th:block> - <th:block th:if="${@permissionService.canManageSessions(edition.id)}"> - <a th:href="@{/lab/{id}/remove(id=${lab.id})}" - class="btn btn-sm btn-danger"> - <i class="fa fa-trash-o" aria-hidden="true"></i> - </a> - </th:block> - <th:block th:if="${@permissionService.canManageSessions(edition.id)}"> - <a href="#" - th:href="@{/lab/{labId}/copy(labId=${lab.id})}" - class="btn btn-sm btn-primary"> - <i class="fa fa-copy" aria-hidden="true"></i> - </a> - </th:block> - </div> - </li> - </th:block> - </ul> + <th:block layout:fragment="script"> + <th:block layout:replace="~{lab/create :: create_script}"></th:block> + + <script> + $(() => { + // Filter on change of sessions + $("#filters select[name='queueSessionTypes']").change(function () { + $("#filters").submit(); + }); + // Filter on change of modules + $("#filters select[name='modules']").change(function () { + $("#filters").submit(); + }); + // Do not reload page when submitting the filters + $("#filters").submit(function (event) { + event.preventDefault(); + const queueSessionTypes = $("select[name='queueSessionTypes']").val().join(","); + const modules = $("select[name='modules']").val().join(","); + window.history.replaceState( + null, + null, + `?queueSessionTypes=${queueSessionTypes}&modules=${modules}` + ); + $.ajax({ + url: `/edition/${$("input[name='edition']").val()}/labs/list`, + type: "get", + data: { + queueSessionTypes: queueSessionTypes, + modules: modules, + }, + success: html => { + $("#labs").replaceWith($(html)); + $("#labs [data-dialog]").click(function () { + openDialog($(this).data("dialog")); + }); + }, + }); + }); + }); + </script> </th:block> -</section> -</body> </html> diff --git a/src/main/resources/templates/edition/view/labs_table.html b/src/main/resources/templates/edition/view/labs_table.html new file mode 100644 index 0000000000000000000000000000000000000000..0947c62c28af2ab1dbc221752e085fd3359ea898 --- /dev/null +++ b/src/main/resources/templates/edition/view/labs_table.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <table + class="subgrid rounded shadow table full_width gap-0 table__to_list" + layout:fragment="table(labs, title)"> + <thead class="subgrid full_width"> + <tr class="subgrid heading full_width"> + <th class="full_width:small" th:text="${title}"></th> + <th class="hidden:small">From</th> + <th class="hidden:small">To</th> + <th class="hidden:small"></th> + </tr> + </thead> + <tbody class="subgrid full_width"> + <tr th:if="${labs.isEmpty()}" class="full_width subgrid"> + <td class="full_width">No sessions</td> + </tr> + <tr th:each="lab : ${labs}" class="subgrid full_width align_items_stretch"> + <td class="flex align_items_center hidden:from_small full_width" style="--gap: 6px"> + <a + class="link" + th:href="@{/lab/{id}(id=${lab.id})}" + th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')}|"></a> + <th:block layout:replace="~{edition/view/labs_table :: tags}"></th:block> + </td> + <td class="flex align_items_center hidden:small" style="--gap: 6px"> + <a class="link" th:href="@{/lab/{id}(id=${lab.id})}" th:text="${lab.name}"></a> + <th:block layout:replace="~{edition/view/labs_table :: tags}"></th:block> + </td> + <td + class="flex align_items_center hidden:small" + th:text="${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')}"></td> + <td + class="flex align_items_center hidden:small" + th:text="${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')}"></td> + <td class="table__actions | full_width:small"> + <th:block th:if="${canManage} and ${shared != true or lab.isShared}"> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-dialog="create-session" + th:data-session="${lab.id}" + data-wizard-type="copy"> + <span class="fa-solid fa-copy margin_right-1"></span> + <span>Copy</span> + </button> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-dialog="create-session" + th:data-session="${lab.id}" + data-wizard-type="edit"> + <span class="fa-solid fa-pencil-alt margin_right-1"></span> + <span>Edit</span> + </button> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-type="negative" + data-dialog="remove-session" + th:data-session="${lab.id}"> + <span class="fa-solid fa-trash-alt margin_right-1"></span> + <span>Delete</span> + </button> + </th:block> + </td> + </tr> + </tbody> + </table> + + <th:block layout:fragment="tags"> + <span th:if="${lab.isShared}" class="chip">Shared</span> + + <span + class="chip" + th:unless="${lab.type.name() == 'REGULAR'}" + th:text="#{|lab.type.${lab.type.name().toLowerCase()}|}"></span> + + <span + class="chip" + data-type="positive" + th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}"> + Open + </span> + </th:block> +</html> diff --git a/src/main/resources/templates/edition/view/labs_tables.html b/src/main/resources/templates/edition/view/labs_tables.html new file mode 100644 index 0000000000000000000000000000000000000000..e7d65606f46f4660326c3625cb747ea8bd556f2d --- /dev/null +++ b/src/main/resources/templates/edition/view/labs_tables.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div id="labs" layout:fragment="tables" class="grid gap-5 columns-4"> + <th:block + layout:replace="~{edition/view/labs_table :: table(labs=${currentSessions}, title='Current sessions')}"></th:block> + <th:block + layout:replace="~{edition/view/labs_table :: table(labs=${upcomingSessions}, title='Upcoming sessions')}"></th:block> + <th:block + layout:replace="~{edition/view/labs_table :: table(labs=${pastSessions}, title='Past sessions')}"></th:block> + + <script> + $(() => { + $("[data-session]").click(async function () { + wizardType = $(this).data("wizard-type"); + $("[data-create-session-wizard]").each(function () { + if (!$(this).data("create-session-wizard").includes(wizardType)) + $(this).addClass("hidden"); + }); + const sessionId = $(this).data("session"); + $("#create-session-id").val(sessionId); + const session = await fetch(`/lab/json/${sessionId}`).then(res => res.json()); + sessionType = session.type; + getForms().forEach(f => getForm(f).trigger("edit", [session])); + getForm("type").trigger("edit", [session]); + }); + + $("[data-dialog='remove-session']").click(function () { + $("#remove-session input[name='id']").val($(this).data("session")); + $("#remove-session-name").text( + $(this).closest("tr").find("td:nth-child(2) > a").text() + ); + }); + }); + </script> + </div> +</html> diff --git a/src/main/resources/templates/edition/view/modules.html b/src/main/resources/templates/edition/view/modules.html index ebb30c39dec8e9dcb0f9272995a0212649f97784..df936339ac2c2081d68f2cf55ac54f001001a032 100644 --- a/src/main/resources/templates/edition/view/modules.html +++ b/src/main/resources/templates/edition/view/modules.html @@ -1,159 +1,181 @@ -<!-- - - 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="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"--> -<!--@thymesVar id="groups", type="java.util.Map<java.lang.Long, java.util.List<nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO>>"--> - -<head> - <title th:text="|Modules - ${edition.course.name} (${edition.name})|"></title> -</head> - -<body> -<section layout:fragment="subcontent"> - <div class="page-sub-header"> - <a class="btn btn-primary float-right" - th:href="@{/edition/{id}/modules/create(id=${edition.id})}" - th:if="${@permissionService.canManageModules(edition.id)}"> - Create Module - </a> - <h3>Modules</h3> - </div> - - <h5 th:if="${#lists.isEmpty(modules)}"> - No modules. - <th:block th:if="${@permissionService.canManageModules(edition.id)}"> - Click on the "Create Module" button to make the first module. - </th:block> - </h5> - - <div th:unless="${#lists.isEmpty(modules)}" class="row"> - <div class="col-md-3 mb-2"> - <div class="list-group" role="tablist"> - <th:block th:each="_module, idx : ${modules}"> - <a class="list-group-item list-group-item-action" - th:classappend="${idx.first ? 'active' : ''}" - role="tab" data-toggle="list" - th:href="'#module-' + ${_module.id}" th:text="${_module.name}"></a> - </th:block> - </div> - </div> - - <div class="col-md-9"> - <div class="tab-content"> - <div th:each="m, idx : ${modules}" - class="tab-pane fade" th:classappend="${idx.first ? 'active show' : ''}" - role="tabpanel" - th:id="'module-' + ${m.id}"> - - <div> - <a class="btn btn-danger btn-sm float-right" - th:if="${@permissionService.canManageModule(m.id)}" - th:href="@{/module/{mId}/remove(mId=${m.id})}" - th:text="|Remove module - ${m.name}|"> - </a> - </div> - - <div> - <a class="btn btn-primary btn-sm float-right mr-4" - th:if="${@permissionService.canManageModule(m.id)}" - th:href="@{/module/{mId}/assignment/create(mId=${m.id})}"> - Create Assignment - </a> - <h3 class="ml-2">Assignments</h3> - </div> - - <h5 th:if="${#lists.isEmpty(m.assignments)}"> - There are no assignments in this module. - </h5> - - <div class="row col-12 mt-2" th:unless="${#lists.isEmpty(m.assignments)}"> - <table class="table"> - <thead class="thead-light"> - <tr> - <th scope="col">#</th> - <th scope="col">Name</th> - <th scope="col">Description</th> - <th scope="col"></th> - </tr> - </thead> - <tr th:each="assignment, aIdx : ${m.getAssignments()}"> - <th scope="row" th:text="${assignment.id}"></th> - <td th:text="${assignment.name}"></td> - <td th:text="${assignment.description.text}"></td> - <td> - <a th:href="@{/assignment/{aId}/remove(aId=${assignment.id})}" - th:if="${@permissionService.canManageModule(m.id)}" - class="btn btn-danger float-right to-be-implemented"> - <i class="fa fa-trash"></i> - </a> - </td> - </tr> - </table> - </div> - - <th:block th:if="${@permissionService.canManageModule(m.id)}"> - <h3 class="ml-2">Student Groups</h3> - - <h5 th:if="${#lists.isEmpty(groups.get(m.id))}"> - There are no student groups in this module, configure them in Portal - </h5> - - <div class="row col-12" th:unless="${#lists.isEmpty(groups.get(m.id))}"> - <table class="table"> - <thead class="thead-light"> - <tr> - <th scope="col" class="fit-width">#</th> - <th scope="col">Name</th> - <th scope="col">Members</th> - <th scope="col" class="fit">Capacity</th> - </tr> - </thead> - <tr th:each="group, gIdx : ${groups.get(m.id)}" - th:with="students = ${@roleDTOService.studentsL1(group.members)}"> - <th class="fit-width" scope="row" th:text="${group.id}"></th> - <td th:text="${group.name}"></td> - <td th:text="${#strings.listJoin(@roleDTOService.names(students), ', ')}"></td> - <td class="fit"> - <span class="badge" - th:classappend="${group.capacity == students.size() ? 'badge-danger' : 'badge-primary'}" - th:text="|${group.capacity} / ${group.capacity}|"></span> - </td> - </tr> - </table> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{edition/view}"> + <th:block layout:fragment="edition-content"> + <div class="flex flex_column:small"> + <div th:class="flex_column"> + <nav data-tabs aria-label="Modules"> + <ul class="nav_list" role="list" data-style="floating"> + <li + th:each="module, iter : ${modules}" + th:data-active="${iter.index == 0}" + th:data-module="${module.id}" + th:data-view="|module-${module.id}|"> + <button th:text="${module.name}"></button> + </li> + </ul> + </nav> + <div class="flex_column"> + <p th:if="${edition.modules.isEmpty()}"> + This edition does not have any modules + </p> + <th:block th:if="${@permissionService.canManageModules(edition.id)}"> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + data-dialog="create-module"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create module</span> + </button> + <div th:unless="${edition.modules.isEmpty()}" class="grid gap-3"> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + data-dialog="edit-module"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit module</span> + </button> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + data-type="negative" + data-dialog="remove-module"> + <span class="margin_right-1 fa-solid fa-trash-alt"></span> + <span>Remove module</span> + </button> </div> </th:block> </div> </div> - </div> - </div> - <script> - $(function () { - $("[data-toggle='tooltip']").tooltip() - }) + <div + th:id="|module-${module.id}|" + class="flex_column flex_grow" + th:each="module, iter : ${modules}" + th:classappend="${iter.index > 0} ? 'hidden' : ''"> + <table class="rounded shadow table"> + <thead> + <tr class="heading"> + <th>Assignments</th> + <th + th:if="${@permissionService.canManageAssignments(edition.id)}" + class="table__actions"> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-dialog="create-assignment"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Add assignment</span> + </button> + </th> + </tr> + </thead> + <tbody> + <tr th:if="${module.assignments.isEmpty()}"> + <td>No assignments</td> + <td></td> + </tr> + <tr + th:each="assignment : ${module.assignments}" + th:data-assignment="${assignment.id}" + th:data-description="${assignment.description?.text}" + th:data-deadline="${assignment.deadline != null} ? ${#temporals.format(assignment.deadline, "yyyy-MM-dd'T'HH:mm")}"> + <td th:text="${assignment.name}"></td> + <td + th:if="${@permissionService.canManageAssignments(edition.id)}" + class="table__actions"> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-dialog="edit-assignment"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit</span> + </button> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-type="negative" + data-dialog="remove-assignment"> + <span class="margin_right-1 fa-solid fa-trash-alt"></span> + <span>Delete</span> + </button> + </td> + </tr> + </tbody> + </table> + + <table class="rounded shadow table"> + <thead> + <tr class="heading"> + <th>Group</th> + <th class="hidden:small">Members</th> + <th>Capacity</th> + </tr> + </thead> + <tbody> + <tr th:if="${module.groups.isEmpty()}"> + <td>No groups</td> + <td></td> + <td></td> + </tr> + <tr th:each="group : ${groups[module.id]}"> + <td th:text="${group.name}"></td> + <td + class="hidden:small" + th:text="${#strings.listJoin(group.members.![person.displayName], ', ')}"></td> + <td th:text="|${group.members.size()} / ${group.capacity}|"></td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <th:block layout:fragment="extra_overlays"> + <th:block layout:replace="~{module/create :: dialog}"></th:block> + <th:block layout:replace="~{module/edit :: dialog}"></th:block> + <th:block layout:replace="~{module/remove :: dialog}"></th:block> + <th:block layout:replace="~{assignment/create :: dialog}"></th:block> + <th:block layout:replace="~{assignment/edit :: dialog}"></th:block> + <th:block layout:replace="~{assignment/remove :: dialog}"></th:block> + </th:block> + + <script layout:fragment="script"> + $(() => { + $("[data-dialog='edit-module']").click(function () { + const module = $(".nav_list [data-active='true']"); + $("#edit-module input[name='module']").val(module.data("module")); + $("#edit-module input[name='name']").val(module.children().text()); + }); + $("[data-dialog='remove-module']").click(function () { + const module = $(".nav_list [data-active='true']"); + $("#remove-module input[name='id']").val(module.data("module")); + $("#remove-module-name").text(module.children().text()); + }); + $("[data-dialog='create-assignment']").click(function () { + $("#create-assignment input[name='module']").val( + $(".nav_list [data-active='true']").data("module") + ); + }); + $("[data-dialog='edit-assignment']").click(function () { + const assignment = $(this).closest("tr"); + $("#edit-assignment input[name='assignment']").val(assignment.data("assignment")); + $("#edit-assignment input[name='name']").val( + assignment.children(":first-child").text() + ); + $("#edit-assignment textarea[name='description']").val( + assignment.data("description") + ); + $("#edit-assignment input[name='deadline']").val(assignment.data("deadline")); + }); + $("[data-dialog='remove-assignment']").click(function () { + const assignment = $(this).closest("tr"); + $("#remove-assignment input[name='id']").val(assignment.data("assignment")); + $("#remove-assignment-name").text(assignment.children(":first-child").text()); + }); + }); </script> -</section> -</body> </html> diff --git a/src/main/resources/templates/edition/view/participants.html b/src/main/resources/templates/edition/view/participants.html index a786b2581d13a776dba4f38f94612ca5eb7e5803..7df0bdfac5142efa662c37304d906757eec37c14 100644 --- a/src/main/resources/templates/edition/view/participants.html +++ b/src/main/resources/templates/edition/view/participants.html @@ -1,199 +1,131 @@ -<!-- - - 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="students" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.PersonSummaryDTO>"--> - -<head> - <title th:text="|Participants - ${edition.course.name} (${edition.name})|"></title> -</head> - -<body> -<section layout:fragment="subcontent" th:with="staffTabActive = ${#httpServletRequest.getParameter('staff-tab-active')}"> - <div class="page-sub-header"> - <div class="float-right" style="margin-right: 10px"> - <a href="#" th:href="@{/edition/{id}/participants/create(id=${edition.id})}" - class="btn btn-secondary"> - Add participant - </a> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{edition/view}"> + <th:block layout:fragment="edition-content"> + <div th:if="${error}" class="banner margin_bottom-5" data-type="error"> + <span class="fa-solid fa-exclamation-circle"></span> + <p th:text="${error}"></p> + <button class="fa-solid fa-xmark" style="margin-left: auto" data-remove-banner></button> </div> - <h3>Participants</h3> - </div> - - <div class="boxed-group"> - <ul class="nav nav-pills"> - <li role="presentation" th:if="${staffTabActive}" class="nav-item"> - <a class="nav-link" - th:href="@{/edition/{id}/participants(id=${edition.id}, staff-tab-active=false)}">Students</a> - </li> - <li role="presentation" th:unless="${staffTabActive}" class="nav-item"> - <a href="#" class="nav-link active">Students</a> - </li> - <li role="presentation" th:if="${staffTabActive}" class="nav-item"> - <a href="#" class="nav-link active">Staff</a> - </li> - <li role="presentation" th:unless="${staffTabActive}" class="nav-item"> - <a class="nav-link" - th:href="@{/edition/{id}/participants(id=${edition.id}, staff-tab-active=true)}">Staff</a> - </li> - </ul> - </div> - <th:block th:if="${staffTabActive}"> - <div class="boxed-group" th:with="teachers = ${@roleDTOService.teachers(edition)}"> - <h3>Teachers</h3> - <div class="boxed-group-inner" - th:if="${#lists.isEmpty(teachers)}"> - There are no teachers participating in this edition. - </div> - - <ul class="list-group" - th:unless="${#lists.isEmpty(teachers)}"> - <li class="list-group-item" - th:each="person : ${teachers}" - th:text="${person.displayName}"> - <div class="float-right" - th:if="${@permissionService.canManageTeachers(edition.id)}"> - <a class="btn btn-xs btn-danger" - th:href="@{/edition/{editionId}/participants/{personId}/remove(editionId=${edition.id},personId=${person.id})}"> - <i class="fa fa-trash-o" aria-hidden="true"></i> - </a> - </div> - </li> - </ul> - </div> - - <div class="boxed-group" th:with="headTAs = ${@roleDTOService.headTAs(edition)}"> - <h3>Manager</h3> - <div class="boxed-group-inner" - th:if="${#lists.isEmpty(headTAs)}"> - There are no managers participating in this edition. - </div> - - <ul class="list-group" - th:unless="${#lists.isEmpty(headTAs)}"> - <li class="list-group-item" - th:each="person : ${headTAs}"> - <th:block th:if="${@permissionService.canViewFeedback(person.id)}"> - <a th:href="@{/feedback/{id}(id=${person.id})}" - th:text="${person.displayName}"></a> - </th:block> - <th:block th:unless="${@permissionService.canViewFeedback(person.id)}" - th:text="${person.displayName}"> - </th:block> - <div class="float-right"> - <a class="btn btn-xs btn-danger" - th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${person.id})}"> - <i class="fa fa-trash-o" aria-hidden="true"></i> - </a> - </div> - </li> - </ul> - </div> - - <div class="boxed-group" th:with="assistants = ${@roleDTOService.assistants(edition)}"> - <h3>Assistants</h3> - <div class="boxed-group-inner" - th:if="${#lists.isEmpty(assistants)}"> - There are no assistants participating in this edition. - </div> - - <ul class="list-group" - th:unless="${#lists.isEmpty(assistants)}"> - <li class="list-group-item" - th:each="person : ${assistants}"> - <th:block - th:if="${@permissionService.canViewFeedback(person.id)}"> - <a th:href="@{/feedback/{id}(id=${person.id})}" - th:text="${person.displayName}"></a> - <!-- <span th:text="'(' + ${feedbackCount.get(person.user.id)} + ')'" class="small">0</span>--> - </th:block> - <th:block th:unless="${@permissionService.canViewFeedback(person.id)}" - th:text="${person.displayName}"> - </th:block> - <div class="float-right"> - <a class="btn btn-xs btn-danger" - th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${person.id})}"> - <i class="fa fa-trash-o" aria-hidden="true"></i> - </a> - </div> - </li> - </ul> - </div> - </th:block> - - <th:block th:unless="${staffTabActive}"> - <form action="" class="form-horizontal" method="get"> - <div class="form-group"> - <label for="filter">Find a student: </label> - <input type="text" id="filter" class="form-control" name="student-search" placeholder="Student name" - th:value="${#httpServletRequest.getParameter('student-search')}"/> + <form id="filters" class="flex_column gap-3 align_items_start margin_bottom-5"> + <input type="hidden" name="edition" th:value="${edition.id}" /> + <div class="searchbox" data-style="floating"> + <input + aria-label="Participant search" + type="text" + name="search" + placeholder="Search" + th:value="${param.search}" /> + <button class="fa-solid fa-search"></button> </div> + <select + class="hidden" + aria-label="Role" + name="roles" + data-select + multiple + data-placeholder="Role" + data-style="floating"> + <option + th:selected="${param.roles?.toString()?.contains('TEACHER')} ?: false" + value="TEACHER"> + Teacher + </option> + <option + th:selected="${param.roles?.toString()?.contains('HEAD_TA')} ?: false" + value="HEAD_TA"> + Head TA + </option> + <option + th:selected="${param.roles?.toString()?.contains('TA')} ?: false" + value="TA"> + TA + </option> + <option + th:selected="${param.roles?.toString()?.contains('STUDENT')} ?: false" + value="STUDENT"> + Student + </option> + </select> </form> - <div class="boxed-group"> - <h3>Students</h3> - <div class="boxed-group-inner" - th:if="${#lists.isEmpty(students)}"> - There are no students participating in this edition. - </div> - - <ul class="list-group" th:unless="${students.size == 0}"> - <li class="list-group-item" th:each="student : ${students}"> - <th:block th:if="${@permissionService.isAdminOrTeacher()}"> - <a th:href="@{/history/edition/{editionId}/student/{id}(editionId=${edition.id},id=${student.id})}" - th:text="${student.displayName}"></a> - </th:block> - <th:block th:unless="${@permissionService.isAdminOrTeacher()}" - th:text="${student.displayName}"> - </th:block> - <!-- Disabled at the moment because of referential constraint errors in labracore (groups) --> -<!-- <div class="float-right">--> -<!-- <a class="btn btn-xs btn-danger"--> -<!-- th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${student.id})}">--> -<!-- <i class="fa fa-trash-o" aria-hidden="true"></i>--> -<!-- </a>--> -<!-- </div>--> - </li> - </ul> - </div> - - <div th:if="${students != null && students.totalPages > 1}"> - <th:block th:replace="pagination :: pagination (page=${students}, size=3)"></th:block> - </div> + <th:block layout:replace="~{edition/view/participants_table :: table}"></th:block> </th:block> - <!-- <div th:if="${cleaned != null}">--> - <!-- <script th:inline="javascript">--> - <!-- /*<![CDATA[*/--> - <!-- var count = /*[[${cleaned}]]*/ '0';--> - <!-- var message = "Cleaned " + count + " users";--> - <!-- alert(message);--> - <!-- /*]]>*/--> - <!-- </script>--> - <!-- </div>--> + <th:block layout:fragment="extra_overlays"> + <th:block layout:replace="~{participant/add :: dialog}"></th:block> + <th:block layout:replace="~{participant/change_role :: dialog}"></th:block> + <th:block layout:replace="~{participant/remove :: dialog}"></th:block> + </th:block> -</section> -</body> + <script layout:fragment="script"> + $(() => { + // Set change role / remove participant dialog info + function setUpParticipantButtons() { + $("[data-dialog='change-role']").click(function () { + const row = $(this).closest("tr"); + $("#change-role input[name='person']").val(row.data("person")); + const name = row.find("td:first-child span:first-child").text(); + $("#change-role-name").text( + name.endsWith("s") || name.endsWith("x") ? name + "'" : name + "'s" + ); + }); + $("[data-dialog='remove-participant']").click(function () { + const row = $(this).closest("tr"); + $("#remove-participant input[name='person']").val(row.data("person")); + const name = row.find("td:first-child span:first-child").text(); + $("#remove-participant-name").text(name); + }); + } + setUpParticipantButtons(); + + // Filter on change of roles + $("select[name='roles']").change(function () { + $("#filters").submit(); + }); + // Filter when user stopped typing + let timeout = null; + $("input[name='search']").keydown(function () { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + $("#filters").submit(); + }, 250); + }); + // Do not reload page when submitting the filters + $("#filters").submit(function (event) { + event.preventDefault(); + const size = $(".pagination").data("page-size"); + const search = $("input[name='search']").val(); + const roles = $("select[name='roles']").val().join(","); + window.history.replaceState( + null, + null, + `?search=${search}&roles=${roles}&page=0&size=${size}` + ); + $.ajax({ + url: `/edition/${$("input[name='edition']").val()}/participants/list`, + type: "get", + data: { + search: search, + roles: roles, + page: 0, + size: size, + }, + success: html => { + $("#participants").replaceWith($(html)); + $("#participants") + .find("[data-dialog]") + .click(function () { + openDialog($(this).data("dialog"), false); + }); + setUpParticipantButtons(); + }, + }); + }); + }); + </script> </html> diff --git a/src/main/resources/templates/edition/view/participants_table.html b/src/main/resources/templates/edition/view/participants_table.html new file mode 100644 index 0000000000000000000000000000000000000000..4c799cf5f265d9e20b4455c8845d4e8552d8a43a --- /dev/null +++ b/src/main/resources/templates/edition/view/participants_table.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div id="participants" class="flex_column gap-5" layout:fragment="table"> + <table class="rounded shadow table table__to_list"> + <thead> + <tr class="heading flex:small space_between"> + <th class="hidden:from_small flex space_between align_items_center"> + Participants + </th> + <th class="hidden:small">Name</th> + <th class="hidden:small">Role</th> + <th class="table__actions"> + <button + class="button padding_inline-5 padding_block-1" + data-style="outlined" + data-dialog="add-participants"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Add participants</span> + </button> + </th> + </tr> + </thead> + <tbody> + <tr th:if="${roles.isEmpty()}"> + <td>No participants found</td> + <td></td> + <td></td> + </tr> + <tr + th:each="role : ${roles}" + class="flex_column:small gap-0" + th:data-person="${role.person.id}"> + <td class="flex:small align_items_center" style="--gap: 6px"> + <span th:text="${role.person.displayName}"></span> + <span + class="chip hidden:from_small" + th:text="#{|role.${role.type.name().toLowerCase()}|}"></span> + </td> + <td + class="hidden:small" + th:text="#{|role.${role.type.name().toLowerCase()}|}"></td> + <td class="table__actions"> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-dialog="change-role"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Change role</span> + </button> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-type="negative" + data-dialog="remove-participant"> + <span class="margin_right-1 fa-solid fa-minus"></span> + <span>Remove</span> + </button> + </td> + </tr> + </tbody> + </table> + + <th:block layout:replace="~{pagination :: pagination(page=${roles}, size=3)}"></th:block> + </div> +</html> diff --git a/src/main/resources/templates/edition/view/questions.html b/src/main/resources/templates/edition/view/questions.html index 043bfe32b473c65fbb2b48ab74e49197e0150c93..e0f13f8bc8bb35a83049364150bfa7bdf7de336c 100644 --- a/src/main/resources/templates/edition/view/questions.html +++ b/src/main/resources/templates/edition/view/questions.html @@ -1,76 +1,79 @@ -<!-- - - 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"--> - -<head> - <title th:text="|Questions - ${edition.course.name} (${edition.name})|"></title> -</head> - -<body> -<section layout:fragment="subcontent"> - - <div class="page-sub-header"> - <h3>Questions</h3> - </div> - - <th:block th:if="${#lists.isEmpty(questions)}"> - No questions for this edition. - </th:block> - - <th:block th:unless="${#lists.isEmpty(questions)}"> - <table class="table"> - <thead class="thead-light"> - <tr> - <th scope="col" class="fit-width">#</th> - <th scope="col">Question</th> - <th scope="col">Answer</th> - <th scope="col">Assignment</th> - <th scope="col">Asked at</th> - <th scope="col" class="fit"></th> - </tr> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{edition/view}"> + <th:block layout:fragment="edition-content"> + <table class="rounded shadow table"> + <thead> + <tr class="heading"> + <th>Question</th> + <th>Answer</th> + <th>Assignment</th> + <th class="table__actions"> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-dialog="create-question"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Add question</span> + </button> + </th> + </tr> </thead> - <tr th:each="question : ${questions}"> - <th class="fit-width" scope="row" th:text="${question.id}"></th> - <td th:text="${question.question}"></td> - <td th:text="${question.answer} ?: 'No answer'"></td> - <td th:text="${question.assignment?.name} ?: 'None'"></td> - <td th:text="${#temporals.format(question.askedAt, 'dd MMMM yyyy HH:mm')}"></td> - <td class="fit"> - <div class="btn-group float-right"> - <a th:href="@{/edition/{edition}/questions/{question}/edit(edition=${edition.id},question=${question.id})}" - class="btn btn-sm btn-secondary"> - <i class="fa fa-pencil" aria-hidden="true"></i> - </a> - <a th:href="@{/edition/{edition}/questions/{question}/remove(edition=${edition.id},question=${question.id})}" - class="btn btn-sm btn-danger"> - <i class="fa fa-trash-o" aria-hidden="true"></i> - </a> - </div> - </td> - </tr> + <tbody> + <tr th:if="${questions.isEmpty()}"> + <td>No questions</td> + <td></td> + <td></td> + <td></td> + </tr> + <tr th:each="question : ${questions}"> + <td th:text="${question.question}"></td> + <td th:text="${question.answer}"></td> + <td th:text="${question.assignment?.name}"></td> + <td class="table__actions"> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-dialog="edit-question" + th:data-question="${question.id}"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit question</span> + </button> + <button + class="button padding_inline-4 padding_block-1" + data-type="negative" + data-style="outlined" + data-dialog="remove-question" + th:data-question="${question.id}"> + <span class="margin_right-1 fa-solid fa-trash-alt"></span> + <span>Delete question</span> + </button> + </td> + </tr> + </tbody> </table> </th:block> -</section> -</body> + <th:block layout:fragment="extra_overlays"> + <th:block th:replace="~{question/create :: dialog}"></th:block> + <th:block th:replace="~{question/edit :: dialog}"></th:block> + <th:block th:replace="~{question/remove :: dialog}"></th:block> + </th:block> + + <script layout:fragment="script"> + $(() => { + $("[data-dialog='edit-question']").click(function () { + const row = $(this).closest("tr"); + $("#edit-question-id").val($(this).data("question")); + $("#edit-question-question").val(row.children()[0].text()); + $("#edit-question-answer").val(row.children()[1].text()); + }); + $("[data-dialog='remove-question']").click(function () { + $("#remove-question-id").val($(this).data("question")); + }); + }); + </script> </html> diff --git a/src/main/resources/templates/edition/view/staff.html b/src/main/resources/templates/edition/view/staff.html new file mode 100644 index 0000000000000000000000000000000000000000..d100d5e8965983e1560b6da2a8209a6bf0a1cc77 --- /dev/null +++ b/src/main/resources/templates/edition/view/staff.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{edition/view}"> + <div layout:fragment="edition-content" class="flex_column"> + <div class="rounded shadow flex_column gap-0"> + <h2 class="heading">Teachers</h2> + <div class="grid info_card auto_fill"> + <span class="padding-5" th:if="${teachers.isEmpty()}">No teachers</span> + <span + class="padding-5" + th:each="teacher : ${teachers.![person]}" + th:text="${teacher.displayName}"></span> + </div> + </div> + + <div class="rounded shadow flex_column gap-0"> + <h2 class="heading">Head TAs</h2> + <div class="grid info_card auto_fill"> + <span class="padding-5" th:if="${headTas.isEmpty()}">No head TAs</span> + <span + class="padding-5" + th:each="headTa : ${headTas.![person]}" + th:text="${headTa.displayName}"></span> + </div> + </div> + + <div class="rounded shadow flex_column gap-0"> + <h2 class="heading">TAs</h2> + <div class="grid info_card auto_fill"> + <span class="padding-5" th:if="${tas.isEmpty()}">No TAs</span> + <span + class="padding-5" + th:each="ta : ${tas.![person]}" + th:text="${ta.displayName}"></span> + </div> + </div> + </div> +</html> diff --git a/src/main/resources/templates/edition_collection/create.html b/src/main/resources/templates/edition_collection/create.html new file mode 100644 index 0000000000000000000000000000000000000000..5e4aa99788f0d9ce1e0e1084e46b2f6734cced1e --- /dev/null +++ b/src/main/resources/templates/edition_collection/create.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="create-edition-collection" layout:fragment="dialog"> + <form th:action="@{/shared-edition}" method="post" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Create course collection</h2> + </div> + <button class="hidden:from_small">Create</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <div class="flex_column gap-1"> + <label class="form__label" for="edition-collection-name">Name</label> + <input + class="textfield" + id="edition-collection-name" + name="name" + placeholder="Collection name" + data-style="outlined" + required /> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="edition-collection-editions"> + Course editions + </label> + <select + id="edition-collection-editions" + name="editions" + data-select + multiple + data-style="outlined" + data-placeholder="Course editions"> + <option + th:each="edition : ${allEditions}" + th:value="${edition.id}" + th:text="|${edition.course.name} - ${edition.name}|"></option> + </select> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create collection</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/error/400.html b/src/main/resources/templates/error/400.html index 3e1e0f6fee5a9ca63fc7b0260910d51902abed8a..e9c6501e936ca95cd297ec57afd10b73ea11909a 100644 --- a/src/main/resources/templates/error/400.html +++ b/src/main/resources/templates/error/400.html @@ -18,22 +18,45 @@ --> <!DOCTYPE html> -<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" - layout:decorate="~{layout}"> -<head> - <title>Queue: Bad Request</title> -</head> - -<body> -<section layout:fragment="content"> - <div class="page-header"> - <h1>Error</h1> - </div> - - <h2>Bad Request</h2> - - Looks like a developer made a booboo. Please report what you did in the Queue Mattermost channel or - through e-mail to eip-ewi@tudelft.nl so we can quickly resolve the issue. -</section> -</body> +<html + lang="en" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <head> + <title>Queue: Bad Request</title> + </head> + + <body> + <section layout:fragment="content"> + <div class="page-header"> + <h1>Error</h1> + </div> + + <h2>Bad Request</h2> + + Looks like a developer made a booboo. Please report what you did in the Queue Mattermost + channel or through e-mail to eip-ewi@tudelft.nl so we can quickly resolve the issue. + </section> + </body> +</html> + +<!DOCTYPE html> +<html + lang="en" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + xmlns:th="http://www.thymeleaf.org" + layout:decorate="~{container}"> + <head> + <title>Queue: Bad Request</title> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <section layout:fragment="content"> + <h1 class="title margin_bottom-3">Bad Request</h1> + <p> + Looks like a developer made a booboo. Please report what you did in the Queue Mattermost + channel or through e-mail to eip-ewi@tudelft.nl, so we can quickly resolve the issue. + </p> + </section> </html> diff --git a/src/main/resources/templates/error/403.html b/src/main/resources/templates/error/403.html index 274119866f0f70e1c8aa10bd4b56db55d4bf443f..a229326bdb521d7581208690a364609358e85a64 100644 --- a/src/main/resources/templates/error/403.html +++ b/src/main/resources/templates/error/403.html @@ -18,24 +18,26 @@ --> <!DOCTYPE html> -<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" - layout:decorate="~{layout}"> -<head> - <title>Queue: Access denied</title> -</head> - -<body> - <section layout:fragment="content"> - <div class="page-header"> - <h1>Error</h1> - </div> - - <h2>Access Denied</h2> - - <p>You do not have permission to enter. This incident will be reported.</p> - - <img src="https://imgs.xkcd.com/comics/incident.png" alt="XKCD" /> - <p>Source: <a href="http://www.xkcd.org">xkcd</a></p> - </section> -</body> +<html + lang="en" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + xmlns:th="http://www.thymeleaf.org" + layout:decorate="~{container}"> + <head> + <title>Queue: Access denied</title> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <section layout:fragment="content"> + <h1 class="title margin_bottom-3">Access Denied</h1> + <p class="margin_bottom-5"> + You do not have permission to enter. This incident will be reported. + </p> + <img src="https://imgs.xkcd.com/comics/incident.png" alt="XKCD" /> + <p class="font-300"> + Source: + <a href="https://www.xkcd.com" class="link">xkcd</a> + </p> + </section> </html> diff --git a/src/main/resources/templates/error/404.html b/src/main/resources/templates/error/404.html index 54f19ec28f55d1928d83c159bc5748d6a71a74e1..d680505ab49d3717cfddb6765e7ce132d5c8d342 100644 --- a/src/main/resources/templates/error/404.html +++ b/src/main/resources/templates/error/404.html @@ -18,17 +18,23 @@ --> <!DOCTYPE html> -<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> - -<body> -<section layout:fragment="content"> - <div class="page-header"> - <h1>Error 404</h1> - </div> - - <h2>Object not found</h2> - - <p>The requested object could not be found, stop looking for things that aren't there :)</p> -</section> -</body> +<html + lang="en" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + xmlns:th="http://www.thymeleaf.org" + layout:decorate="~{container}"> + <head> + <title>Queue: Not Found</title> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <section layout:fragment="content"> + <h1 class="title margin_bottom-3">Not Found</h1> + <p> + The requested object could not be found + <br /> + <span class="font-300">Stop looking for things that aren't there (:</span> + </p> + </section> </html> diff --git a/src/main/resources/templates/error/422.html b/src/main/resources/templates/error/422.html index 656d7a06df65a6595ef59bd225701a697fa739e9..05c943ec6515feddc1ce0fa514b0c8feb830f4a0 100644 --- a/src/main/resources/templates/error/422.html +++ b/src/main/resources/templates/error/422.html @@ -18,21 +18,63 @@ --> <!DOCTYPE html> -<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}"> -<head> - <title>Queue: Error</title> -</head> - -<body> - <section layout:fragment="content"> - <div class="page-header"> - <h1>Error</h1> - </div> - <div class="jumbotron alert-danger"> - <h1>Something went wrong during validation of an entity.</h1> - <h5>Are you trying to break Queue?</h5> - </div> - </section> -</body> +<html + lang="en" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + xmlns:th="http://www.thymeleaf.org" + layout:decorate="~{container}"> + <head> + <title>Queue: Error</title> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <section layout:fragment="content"> + <h1 class="title margin_bottom-3">Error</h1> + <p> + <span>Something went wrong during validation of an entity.</span><br> + <span class="font-300">Are you trying to break Queue?</span> + </p> + </section> </html> +<!-- + + 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" + xmlns:th="http://www.thymeleaf.org" + layout:decorate="~{container}"> + <head> + <title>Queue: Validation Error</title> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <section layout:fragment="content"> + <h1 class="title margin_bottom-3">Validation Error</h1> + <p> + Something went wrong during validation of an entity. + <br /> + <span class="font-300">Are you trying to break Queue?</span> + </p> + </section> +</html> diff --git a/src/main/resources/templates/error/500.html b/src/main/resources/templates/error/500.html index ce6bfb8b82800c99dda469904769966a4efa1934..6b0d5d4e0de644f599d47ebafc7638d905384e61 100644 --- a/src/main/resources/templates/error/500.html +++ b/src/main/resources/templates/error/500.html @@ -18,24 +18,24 @@ --> <!DOCTYPE html> -<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}"> - -<head> - <title>Queue: Error</title> -</head> - -<body> - <section layout:fragment="content"> - <div class="page-header"> - <h1>Error</h1> - </div> - <div class="jumbotron alert-danger"> - <h1> - Oops. Something went wrong on our end...<br> - If this occurs again, contact us with a description of what you did before we show you this screen - </h1> - </div> - </section> -</body> +<html + lang="en" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + xmlns:th="http://www.thymeleaf.org" + layout:decorate="~{container}"> + <head> + <title>Queue: Error</title> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <section layout:fragment="content"> + <h1 class="title margin_bottom-3">Error</h1> + <p> + Oops. Something went wrong on our end... + <br /> + If this occurs again, contact us with a description of what you did before we show you + this screen + </p> + </section> </html> - diff --git a/src/main/resources/templates/error/error.html b/src/main/resources/templates/error/error.html index 36a0ad6eb251e88ee2524e55c1164ac4c095ef51..18856e0c606dc5c79fd80ae9ecc6e08b547dcff0 100644 --- a/src/main/resources/templates/error/error.html +++ b/src/main/resources/templates/error/error.html @@ -18,20 +18,19 @@ --> <!DOCTYPE html> -<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}"> -<head> - <title>Queue: Error</title> -</head> +<html + lang="en" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + xmlns:th="http://www.thymeleaf.org" + layout:decorate="~{container}"> + <head> + <title>Queue: Error</title> + </head> -<body> - <section layout:fragment="content"> - <div class="page-header"> - <h1>Error</h1> - </div> - <div class="jumbotron alert-danger"> - <h1>Oops. Something went wrong...</h1> - </div> - </section> -</body> -</html> + <div layout:fragment="breadcrumbs" class="hidden"></div> + <section layout:fragment="content"> + <h1 class="title margin_bottom-3">Error</h1> + <p>Oops. Something went wrong...</p> + </section> +</html> diff --git a/src/main/resources/templates/footer.html b/src/main/resources/templates/footer.html new file mode 100644 index 0000000000000000000000000000000000000000..472daad94c50d805f1370974d0901087701d25ca --- /dev/null +++ b/src/main/resources/templates/footer.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <footer class="footer" layout:fragment="footer"> + <div class="footer__content"> + <div class="footer__logo"> + <img height="50" alt="TU Delft logo" src="/img/tudelft-logo.png" /> + <span class="text_muted font-400" style="white-space: nowrap"> + © Delft University of Technology + </span> + </div> + <nav aria-label="Footer" class="footer__links"> + <a class="link" style="white-space: nowrap" th:href="@{/privacy}"> + Privacy statement + </a> + <a class="link" th:href="@{/about}">About</a> + </nav> + </div> + </footer> +</html> diff --git a/src/main/resources/templates/header.html b/src/main/resources/templates/header.html new file mode 100644 index 0000000000000000000000000000000000000000..d3bc78c3443f86f3275d2915e9c9e7486f2e3098 --- /dev/null +++ b/src/main/resources/templates/header.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <header class="header" data-collapsed="true" layout:fragment="header"> + <div class="header__content"> + <div class="header__top"> + <a class="header__logo" th:href="@{/}">Queue</a> + <button class="header__hamburger fa-solid fa-bars"></button> + </div> + + <div class="header__controls"> + <nav class="header__links"> + <th:block th:unless="${#authenticatedP == null}"> + <a th:href="@{/}" th:data-active="${page == 'my-courses'}">My courses</a> + <a th:href="@{/editions}" th:data-active="${page == 'catalog'}"> + Catalogue + </a> + <a + th:if="${@permissionService.canViewRequests()}" + th:href="@{/requests}" + th:data-active="${page == 'requests'}"> + Requests + </a> + </th:block> + </nav> + + <div class="header__right"> + <th:block th:if="${#authenticatedP == null}"> + <a class="header__user" th:href="@{/login}"> + <span class="fa-solid fa-door-open"></span> + <span>Log in</span> + </a> + </th:block> + <th:block th:unless="${#authenticatedP == null}"> + <button class="header__user"> + <span th:text="${#authenticatedP.displayName}"></span> + <span class="fa-solid fa-chevron-down"></span> + </button> + <nav class="header__dropdown"> + <a th:href="@{/history}"> + <span class="margin_right-1 fa-solid fa-clock"></span> + <span>Request history</span> + </a> + <a + th:if="${@permissionService.canViewRequests()}" + th:href="@{/feedback}"> + <span class="margin_right-1 fa-solid fa-star"></span> + <span>Feedback</span> + </a> + <form th:action="@{/logout}" method="post"> + <button> + <span class="margin_right-1 fa-solid fa-door-closed"></span> + <span>Log out</span> + </button> + </form> + </nav> + </th:block> + </div> + </div> + </div> + </header> + + <script layout:fragment="header_script"> + $(() => { + $(".header__hamburger").click(function () { + const header = $(".header"); + const collapsed = header.data("collapsed"); + header.data("collapsed", !collapsed); + header.attr("data-collapsed", !collapsed); + }); + $(".header__user").click(function () { + $(this).focus(); + }); + }); + </script> +</html> diff --git a/src/main/resources/templates/history/index.html b/src/main/resources/templates/history/index.html index 14f072e6dcd2c2c502892eeb1054bad53b77fa14..0bb3a1eeaa400ddfb5267a6089bd299b18ad5d7d 100644 --- a/src/main/resources/templates/history/index.html +++ b/src/main/resources/templates/history/index.html @@ -1,58 +1,42 @@ -<!-- - - 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="~{layout}"> - -<!--@thymesVar id="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.RequestViewDTO>"--> - -<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 active">History</li> - </ol> - </nav> - - <div class="page-header"> - <h1>My Requests</h1> - </div> -</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})"> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <head> + <title>Request history</title> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <th:block layout:fragment="content"> + <h1 class="title margin_bottom-5">Request History</h1> + + <div class="flex_column"> + <div class="flex_column align_items_start:from_small"> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + data-dialog="filters"> + <span class="margin_right-1 fa-solid fa-filter"></span> + <span>Filters</span> + <span th:text="|(${filter.countActiveFilters()})|"></span> + </button> + </div> + + <th:block layout:fragment="request_list"> + <th:block + layout:replace="~{request/table :: table(showAssistants=true)}"></th:block> </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> + </th:block> - <div class="justify-content-center"> - <th:block th:replace="pagination :: pagination (page=${requests}, size=3)"> + <th:block layout:fragment="overlays"> + <th:block th:with="returnPath = '/history'"> + <th:block layout:replace="~{request/filters :: dialog}"></th:block> </th:block> - </div> -</section> -</body> + </th:block> + + <th:block layout:fragment="script"></th:block> </html> diff --git a/src/main/resources/templates/home/about.html b/src/main/resources/templates/home/about.html index 8cd0a24c96b9a3f2e9b6e63d22649709ade67ad5..113e9793b71a39999a70a85ddb7225b71df8fd65 100644 --- a/src/main/resources/templates/home/about.html +++ b/src/main/resources/templates/home/about.html @@ -18,77 +18,94 @@ --> <!DOCTYPE html> -<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> +<html + lang="en" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <div layout:fragment="breadcrumbs" class="hidden"></div> -<body> -<section layout:fragment="content"> - <div class="page-header"> - <h1> About Queue </h1> - </div> - <div class="row"> - <div class="col-sm-12 col-md-6"> - <p class="h3"> What is Queue? </p> - <p> - Queue is a system that serves as a replacement for a physical queue. It was originally intended for - lab sessions, where students work on their own and can ask questions to student assistants. As the number - of students grows, it becomes harder and harder to keep track of who has questions, and who has the first turn. + <section layout:fragment="content"> + <h1 class="title margin_bottom-5">About Queue</h1> - In its simplest form, Queue allows students to create a request and allows student assistants to pick up these - requests, in order. - </p> - </div> + <div class="grid gap-7 columns-1:small" style="text-align: justify"> + <div> + <h2 class="subtitle">What is Queue?</h2> + <p class="margin_bottom-5"> + Queue is a system that serves as a replacement for a physical queue. It was + originally intended for lab sessions, where students work on their own and can + ask questions to student assistants. As the number of students grows, it becomes + harder and harder to keep track of who has questions, and who has the first + turn. In its simplest form, Queue allows students to create a request and allows + student assistants to pick up these requests, in order. + </p> - <div class="col-sm-12 col-md-6"> - <p class="h3"> Why Queue? </p> - <p> - Queue is a simple to use tool with many advanced capabilities and customisation options included. With the problem - of managing and keeping track of questions out of the way, the student assistants can do what they do best: teach. - </p> - </div> - </div> + <h2 class="subtitle">Why Queue?</h2> + <p class="margin_bottom-5"> + Queue is a simple to use tool with many advanced capabilities and customisation + options included. With the problem of managing and keeping track of questions + out of the way, the student assistants can do what they do best: teach. + </p> - <div class="row"> - <div class="col-sm-12 col-md-6"> - <p class="h3"> How does Queue work? </p> - <p> - Queue transforms a physical queue into a virtual one by taking all the requests of the students and putting them - in the right order. Student assistants can then, one by one, take those requests and handle them individually. - Additionally, (the) Queue is also highly customisable and allows teachers to create labs for specific situations - such as exams or feedback sessions. - </p> - </div> + <h2 class="subtitle">How does Queue work?</h2> + <p> + Queue transforms a physical queue into a virtual one by taking all the requests + of the students and putting them in the right order. Student assistants can + then, one by one, take those requests and handle them individually. + Additionally, (the) Queue is also highly customisable and allows teachers to + create labs for specific situations such as exams or feedback sessions. + </p> + </div> - <div class="col-sm-12 col-md-6"> - <p class="h3"> History of Queue </p> - <p> - Queue started as a literal physical queue that eventually evolved into a management system with a spreadsheet. - This short-term solution wasn't the best and thus the idea of Queue was born. Shortly after, a student was hired - and Queue 0.1 was not only a dream anymore. After that, a Surf grant helped to further development and the team - grew to a size of 8 students. - </p> - </div> - </div> + <div> + <h2 class="subtitle">History of Queue</h2> + <p class="margin_bottom-5"> + Queue started as a literal physical queue that eventually evolved into a + management system with a spreadsheet. This short-term solution wasn't the best + and thus the idea of Queue was born. Shortly after, a student was hired and + Queue 0.1 was not only a dream anymore. After that, a Surf grant helped to + further development and the team grew to a size of 8 students. + </p> - <div class="row"> - <div class="col-sm-12 col-md-6"> - <p class="h3"> How do I report bugs? </p> - <p> - Because Queue is an open-source project, one can go to the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue" target="_blank"> - Queue repository</a>, choose Issues in the menu and open a new issue. There are templates for submitting - features and bugs and it should be straightforward from there. - </p> - </div> + <h2 class="subtitle">How do I report bugs?</h2> + <p class="margin_bottom-5"> + Because Queue is an open-source project, one can go to the + <a + class="link" + href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue" + target="_blank"> + Queue repository + </a> + , choose Issues in the menu and open a new issue. There are templates for + submitting features and bugs and it should be straightforward from there. + </p> - <div class="col-sm-12 col-md-6"> - <p class="h3"> How do I contribute? </p> - <p> - There are two main ways of contributing: working on an open-standing issue or tackling a bug bounty and - earn something extra. The Queue contains an <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues" target="_blank"> - issues list</a> and a filter for all the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues?label_name[]=BugBounty" target="_blank">bug bounties</a>. - For reading the rules of contributing check the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/blob/development/CONTRIBUTING.md" target="_blank">CONTRIBUTING.md</a>. - </p> + <h2 class="subtitle">How do I contribute?</h2> + <p> + There are two main ways of contributing: working on an open-standing issue or + tackling a bug bounty and earn something extra. The Queue contains an + <a + class="link" + href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues" + target="_blank"> + issues list + </a> + and a filter for all the + <a + class="link" + href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues?label_name[]=BugBounty" + target="_blank"> + bug bounties + </a> + . For reading the rules of contributing check the + <a + class="link" + href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/blob/development/CONTRIBUTING.md" + target="_blank"> + CONTRIBUTING.md + </a> + . + </p> + </div> </div> - </div> -</section> -</body> + </section> </html> diff --git a/src/main/resources/templates/home/dashboard.html b/src/main/resources/templates/home/dashboard.html index 0a5a9dbd25d8ce91dffd71cb3955319a2f9d713b..9c7a0dc671a65b8d6b1ac276e26a140b91a524c8 100644 --- a/src/main/resources/templates/home/dashboard.html +++ b/src/main/resources/templates/home/dashboard.html @@ -1,275 +1,264 @@ -<!-- - - 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="~{layout}"> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> <head> - <link href='/webjars/fullcalendar/main.css' rel='stylesheet' /> - <script src='/webjars/fullcalendar/main.js'></script> + <link href="/webjars/fullcalendar/main.css" rel="stylesheet" /> + <script src="/webjars/fullcalendar/main.js"></script> <script src="/webjars/fullcalendar/locales/nl.js"></script> </head> -<!--@thymesVar id="user" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"--> -<!--@thymesVar id="editions" type="java.util.Map<java.lang.Long, nl.tudelft.labracore.api.dto.EditionDetailsDTO>"--> -<!--@thymesVar id="runningEditions" type="java.util.List<nl.tudelft.labracore.api.dto.EditionSummaryDTO>"--> + <div layout:fragment="breadcrumbs" class="hidden"></div> -<!--@thymesVar id="activeRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"--> -<!--@thymesVar id="finishedRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"--> -<!--@thymesVar id="archivedRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"--> + <th:block layout:fragment="content"> + <h1 class="title">Enrolled Courses</h1> -<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 data-tabs class="tabs margin_bottom-5"> + <button data-active="true" data-view="list">List</button> + <button data-active="false" data-view="calendar-view">Calendar</button> </nav> - <div class="page-header"> - <h1>Home</h1> - </div> - <div style="margin-bottom: 1rem" th:if="${@permissionService.isAdminOrTeacher()}"> - <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 id="list" class="flex_column"> + <div th:if="${success}" class="banner" data-type="check"> + <span class="fa-solid fa-check-circle"></span> + <span th:text="${success}"></span> + <button + class="fa-solid fa-xmark" + style="margin-left: auto" + data-remove-banner></button> </div> - <div th:replace="~{shared-edition/create/create-shared-edition :: overlay}"></div> - </div> - <ul class="nav nav-tabs" id="overview-tabs" role="tablist"> - <li class="nav-item"> - <a class="nav-link active" id="overview-tab" data-toggle="tab" href="#overview" - role="tab" aria-controls="overview" aria-selected="true">Course overview</a> - </li> - <li class="nav-item"> - <a class="nav-link" id="calendar-tab" data-toggle="tab" href="#calendar" role="tab" - aria-controls="calendar" aria-selected="false">Calendar</a> - </li> - <li class="nav-item" th:if="${@permissionService.isAdmin()}"> - <a class="nav-link" id="admin-tab" data-toggle="tab" href="#admin" role="tab" - aria-controls="admin" aria-selected="false">Admin</a> - </li> - </ul> - - <div class="tab-content mt-2"> - <div class="tab-pane fade" id="calendar" role="tabpanel" - aria-labelledby="calendar-tab"> - <h2>Calendar</h2> - <div id="calendar-view"></div> - <script th:inline="javascript"> - document.addEventListener('DOMContentLoaded', function () { - let calendarEl = document.getElementById('calendar-view'); - let calendar = new FullCalendar.Calendar(calendarEl, { - // plugins: [listPlugin], - headerToolbar: { - left: 'timeGridWeek,timeGridDay,listDay', - center: 'title', - right: 'today prev,next' - }, - initialView: 'timeGridWeek', - nowIndicator: true, - businessHours: { - daysOfWeek: [1, 2, 3, 4, 5], - startTime: '08:45', - endTime: '17:45' - }, - eventDisplay: 'block', - }); - let sessions = /*[[${sessions}]]*/; - sessions.forEach(dto => { - const event = { - title: dto.session.name, - start: dto.session.start, - end: dto.session.end, - description: dto.session.description, - url: "/lab/" + dto.id - } - switch (dto.type) { - case "SLOTTED": - event['backgroundColor'] = "yellow" - event['textColor'] = "black" - calendar.addEvent({ - title: dto.session.name + " slot selection", - start: dto.selectionOpensAt, - backgroundColor: "yellow", - textColor: "black" - }) - break; - case "EXAM": - event['backgroundColor'] = "red" - calendar.addEvent({ - title: dto.session.name + " slot selection", - start: dto.selectionOpensAt, - backgroundColor: "red", - }) - break; - } - calendar.addEvent(event) - }) - calendar.render(); - $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) { - calendar.updateSize(); - }) - }) - </script> + <div + th:if="${@permissionService.canCreateEdition()}" + class="flex gap-3 | flex_column:small"> + <button + class="padding_inline-5 padding_block-3 button" + data-style="floating" + data-dialog="create-edition"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create course edition</span> + </button> + <button + class="padding_inline-5 padding_block-3 button" + data-style="floating" + data-dialog="create-edition-collection"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create course collection</span> + </button> </div> - <div class="tab-pane fade show active" id="overview" role="tabpanel" - aria-labelledby="overview-tabs"> - <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>? - </div> + <div class="rounded shadow flex_column gap-0"> + <h2 class="heading">Active courses</h2> + <ul class="list" role="list"> + <li th:if="${activeRoles.isEmpty()}"> + You are not enrolled in any active courses. + </li> - <ul class="list-group"> - <li class="list-group-item" th:each="role : ${activeRoles}" - 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> - <ul th:unless="${#lists.isEmpty(labs)}"> - <th:block th:each="sess : ${edition.sessions}"> - <li th:each="lab : ${labs[sess.id]}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'"> - <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" - th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> - </a> - <span th:if="${lab.isShared}" class="badge badge-pill badge-info text-white">Shared lab</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.open()}">Active</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.closed()}">Completed</span> + <li + class="flex_column gap-2" + th:each="role : ${activeRoles}" + th:with="edition = ${editions[role.edition.id]}"> + <div class="flex align_items_center" style="--gap: 6px"> + <a + class="link" + th:href="@{/edition/{id}(id=${role.edition.id})}" + th:text="|${editions[role.edition.id].course.name} - ${role.edition.name}|"></a> + <span + class="chip" + th:text="#{|role.${role.type.toString().toLowerCase()}|}"></span> + </div> - <span class="badge badge-pill badge-danger text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> - <span class="badge badge-pill badge-warning text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> + <ul + class="flex_column gap-1 margin_left-5" + th:unless="${edition.sessions.isEmpty()}"> + <th:block th:each="sess : ${edition.sessions}"> + <li + class="flex align_items_center" + style="--gap: 6px" + th:each="lab : ${labs[sess.id]}"> + <a + class="link" + th:href="@{/lab/{id}(id=${lab.id})}" + th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"></a> - <span class="badge badge-pill badge-warning text-muted" - th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> - </li> - </th:block> - </ul> - </li> - <li class="list-group-item" th:each="shared : ${sharedEditions}"> - <a th:href="@{/shared-edition/{id}(id=${shared.id})}" - th:text="|${shared.getName()}|"></a> - <span class="badge badge-pill badge-info text-white"> - Shared edition - </span> - <ul th:unless="${#lists.isEmpty(labs)}"> - <li th:each="lab : ${sharedLabs[shared.id]}" - th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'"> - <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" - th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> - </a> - <span th:if="${lab.isShared}" class="badge badge-pill badge-info text-white">Shared lab</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.open()}">Active</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.closed()}">Completed</span> + <span th:if="${lab.isShared}" class="tag">Shared</span> - <span class="badge badge-pill badge-danger text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> - <span class="badge badge-pill badge-warning text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> + <span + class="chip" + data-type="positive" + th:if="${lab.slot.open()}"> + Active + </span> + <span class="chip" th:if="${lab.slot.closed()}">Completed</span> - <span class="badge badge-pill badge-warning text-muted" - th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> - </li> - </ul> - </li> - </ul> - </div> + <span + class="chip" + th:unless="${lab.type.name() == 'REGULAR'}" + th:text="#{|lab.type.${lab.type.name().toLowerCase()}|}"></span> - <div class="boxed-group" th:if="${!#lists.isEmpty(finishedRoles)}"> - <h3>Finished courses</h3> + <span + class="chip" + data-type="positive" + th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}"> + Open + </span> + </li> + </th:block> + </ul> + </li> - <ul class="list-group"> - <li class="list-group-item" th:each="role : ${finishedRoles}" - 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> + <li class="flex_column gap-2" th:each="collection : ${sharedEditions}"> + <div class="flex align_items_center" style="--gap: 6px"> + <a + class="link" + th:href="@{/shared-edition/{id}(id=${collection.id})}" + th:text="${collection.name}"></a> + <span class="chip">Shared</span> + </div> - <div class="boxed-group" th:if="${!#lists.isEmpty(archivedRoles)}"> - <h3>Archived courses</h3> + <ul + class="flex_column gap-1 margin_left-5" + th:unless="${sharedLabs[collection.id].isEmpty()}"> + <th:block th:each="sess : ${sharedLabs[collection.id]}"> + <li + class="flex align_items_center" + style="--gap: 6px" + th:each="lab : ${labs[sess.id]}"> + <a + class="link" + th:href="@{/lab/{id}(id=${lab.id})}" + th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"></a> - <ul class="list-group"> - <th:block th:each="role : ${archivedRoles}" th:with="edition = ${editions.get(role.edition.id)}"> - <li class="list-group-item"> - <a th:href="@{/edition/{id}(id=${edition.id})}" - th:text="|${edition.course.name} (${edition.name})|"></a> - <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span> - </li> - </th:block> - </ul> - </div> - </div> + <span th:if="${lab.isShared}" class="tag">Shared</span> - <div class="tab-pane fade" id="admin" role="tabpanel" aria-labelledby="admin-tab" - th:if="${@permissionService.isAdmin()}"> - <div class="boxed-group" > - <h3>Admin overview - Labs running today:</h3> - <div class="boxed-group-inner" th:if="${#lists.isEmpty(runningLabs)}"> - None. - </div> + <span + class="chip" + data-type="positive" + th:if="${lab.slot.open()}"> + Active + </span> + <span class="chip" th:if="${lab.slot.closed()}">Completed</span> - <ul class="list-group"> - <li th:each="lab : ${runningLabs}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'" class="list-group-item"> - <a href="#" th:href="@{/lab/{id}(id=${lab.id})}" - th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"> - </a> + <span + class="chip" + th:unless="${lab.type.name() == 'REGULAR'}" + th:text="#{|lab.type.${lab.type.name().toLowerCase()}|}"></span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.open()}">Active</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.slot.closed()}">Completed</span> + <span + class="chip" + data-type="positive" + th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}"> + Open + </span> + </li> + </th:block> + </ul> + </li> + </ul> + </div> - <span class="badge badge-pill badge-danger text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span> - <span class="badge badge-pill badge-info text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span> - <span class="badge badge-pill badge-warning text-white" - th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span> + <div class="rounded shadow flex_column gap-0" th:unless="${finishedRoles.isEmpty()}"> + <h2 class="heading">Finished courses</h2> + <ul class="list" role="list"> + <li + class="flex align_items_center" + style="--gap: 6px" + th:each="role : ${finishedRoles}"> + <a + class="link" + th:href="@{/edition/{id}(id=${role.edition.id})}" + th:text="|${editions[role.edition.id].course.name} - ${role.edition.name}|"></a> + <span + class="chip" + th:text="#{|role.${role.type.toString().toLowerCase()}|}"></span> + </li> + </ul> + </div> - <span class="badge badge-pill badge-warning text-muted" - th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span> - </li> - </ul> - </div> + <div class="rounded shadow flex_column gap-0" th:unless="${archivedRoles.isEmpty()}"> + <h2 class="heading">Archived courses</h2> + <ul class="list" role="list"> + <li + class="flex align_items_center" + style="--gap: 6px" + th:each="role : ${archivedRoles}"> + <a + class="link" + th:href="@{/edition/{id}(id=${role.edition.id})}" + th:text="|${editions[role.edition.id].course.name} - ${role.edition.name}|"></a> + <span + class="chip" + th:text="#{|role.${role.type.toString().toLowerCase()}|}"></span> + </li> + </ul> </div> </div> - </section> -</body> + + <div id="calendar-view"></div> + <script th:inline="javascript"> + document.addEventListener('DOMContentLoaded', function () { + let calendarEl = document.getElementById('calendar-view'); + let calendar = new FullCalendar.Calendar(calendarEl, { + // plugins: [listPlugin], + headerToolbar: { + left: 'timeGridWeek,timeGridDay,listDay', + center: 'title', + right: 'today prev,next' + }, + initialView: 'timeGridWeek', + nowIndicator: true, + businessHours: { + daysOfWeek: [1, 2, 3, 4, 5], + startTime: '08:45', + endTime: '17:45' + }, + eventDisplay: 'block', + }); + let sessions = /*[[${sessions}]]*/; + sessions.forEach(dto => { + const event = { + title: dto.session.name, + start: dto.session.start, + end: dto.session.end, + description: dto.session.description, + url: "/lab/" + dto.id + } + switch (dto.type) { + case "SLOTTED": + event['backgroundColor'] = "yellow" + event['textColor'] = "black" + calendar.addEvent({ + title: dto.session.name + " slot selection", + start: dto.selectionOpensAt, + backgroundColor: "yellow", + textColor: "black" + }) + break; + case "EXAM": + event['backgroundColor'] = "red" + calendar.addEvent({ + title: dto.session.name + " slot selection", + start: dto.selectionOpensAt, + backgroundColor: "red", + }) + break; + } + calendar.addEvent(event) + }) + calendar.render(); + $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) { + calendar.updateSize(); + }); + $("#calendar-view").addClass("hidden"); + }) + </script> + </th:block> + + <th:block layout:fragment="overlays"> + <th:block layout:replace="~{edition/create :: dialog}"></th:block> + <th:block layout:replace="~{edition_collection/create :: dialog}"></th:block> + <th:block layout:replace="~{course/request :: dialog}"></th:block> + </th:block> </html> diff --git a/src/main/resources/templates/home/feedback.html b/src/main/resources/templates/home/feedback.html index 14a34db2ce22fa4340b28517f36e78971e2eeb36..9fb053bbf1104ea850e3392b331f5d367945c10a 100644 --- a/src/main/resources/templates/home/feedback.html +++ b/src/main/resources/templates/home/feedback.html @@ -1,135 +1,110 @@ -<!-- - - 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="~{layout}"> - -<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.lib.security.user.Person"--> - -<!--@thymesVar id="assistant" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"--> -<!--@thymesVar id="feedback" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.FeedbackViewDTO>"--> - -<head> - <title th:text="|Feedback for ${assistant.displayName}|"></title> - - <script src="/webjars/chartjs/Chart.min.js"></script> -</head> - -<body> -<section layout:fragment="content"> - <div class="page-sub-header"> - <h3 th:text="|Feedback for ${assistant.displayName}|"></h3> - </div> - - <div th:if="${feedback.isEmpty()}"> - <h4 th:if="${#authenticatedP.id == assistant.id}"> - You have no feedback yet. - </h4> - <h4 th:if="${#authenticatedP.id != assistant.id}"> - There is no feedback for this assistant yet. - </h4> - </div> - - <div class="row" th:unless="${feedback.isEmpty()}"> - <div class="col-md-6"> - <div class="panel panel-default container"> - <h4 class="mt-2">Feedback comments</h4> - - <blockquote class="blockquote" - th:each="fb : ${feedback}" - th:if="${fb.feedback != null && !fb.feedback.isEmpty()}"> - <p th:text="${fb.feedback}"></p> - <footer class="blockquote-footer" - th:if="${fb.rating != null}" - th:unless="${@permissionService.canViewDeanonimizedFeedback()}"> - <span th:text="|Feedback of ${fb.rating + '/5'}|"></span> - </footer> - <footer class="blockquote-footer" th:if="${@permissionService.canViewDeanonimizedFeedback()}"> - <span>Feedback </span> - <span th:if="${fb.rating != null}" th:text="|of ${fb.rating + '/5'} |"></span> - <span th:text="|by ${fb.groupName} on Request|"></span> - <a th:text="${'#' + fb.request.id}" - th:href="@{/request/{id}(id = ${fb.request.id})}"></a><br> - <span th:text="|Last updated on ${#temporals.format(fb.lastUpdatedAt, 'dd-MM-yyyy')}, created ${#temporals.format(fb.createdAt, 'dd-MM-yyyy')}|"></span> - </footer> - </blockquote> - </div> - - <th:block th:replace="pagination :: pagination (page=${feedback}, size=3)"> - </th:block> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <head> + <title>Feedback</title> + <script src="/webjars/chartjs/2.7.0/Chart.min.js"></script> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <th:block layout:fragment="content"> + <h1 class="title margin_bottom-5" th:text="|Feedback for ${assistant.displayName}|"></h1> + + <div th:if="${feedbackHidden}" class="banner margin_bottom-5" data-type="info"> + <span class="fa-solid fa-info-circle"></span> + <p>To keep feedback anonymous, some recent feedback items are not shown.</p> </div> - <div class="col-md-6"> - <div class="container panel panel-default"> - <h4 class="mt-2">Overview (5 is most positive)</h4> - - <canvas id="feedback-histogram"></canvas> - - <script th:inline="javascript" type="text/javascript"> - //<![CDATA[ - const stars = /*[[${stars}]]*/ [0, 0, 0, 0, 0] - - const ctx = document.getElementById("feedback-histogram").getContext('2d'); - new Chart(ctx, { - type: 'bar', - data: { - labels: ['1', '2', '3', '4', '5'], - datasets: [{ - label: '# of Votes', - data: stars, - backgroundColor: [ - 'rgba(255, 99, 132, 0.2)', - 'rgba(54, 162, 235, 0.2)', - 'rgba(255, 206, 86, 0.2)', - 'rgba(75, 192, 192, 0.2)', - 'rgba(153, 102, 255, 0.2)', - 'rgba(255, 159, 64, 0.2)' - ], - borderColor: [ - 'rgba(255, 99, 132, 1)', - 'rgba(54, 162, 235, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(75, 192, 192, 1)', - 'rgba(153, 102, 255, 1)', - 'rgba(255, 159, 64, 1)' + <div class="flex align_items_start | flex_column:small align_items_stretch:small"> + <div class="shadow rounded info_card" style="max-width: 20rem"> + <h2 class="heading">Overview</h2> + <div class="padding-5"> + <canvas + id="overview" + width="320" + height="320" + aria-label="Feedback rating overview graph" + role="img"></canvas> + <script> + /*<![CDATA[*/ + const scores = /*[[${stars}]]*/ [0, 0, 0, 0, 0]; + /*]]>*/ + </script> + <script> + $(() => { + const ctx = document.getElementById("overview").getContext("2d"); + const data = { + labels: [1, 2, 3, 4, 5], + datasets: [ + { + label: "#Score", + data: scores, + backgroundColor: [ + "rgba(255, 99, 132, 0.2)", + "rgba(255, 159, 64, 0.2)", + "rgba(255, 205, 86, 0.2)", + "rgba(75, 192, 192, 0.2)", + "rgba(54, 162, 235, 0.2)", + ], + borderColor: [ + "rgb(255, 99, 132)", + "rgb(255, 159, 64)", + "rgb(255, 205, 86)", + "rgb(75, 192, 192)", + "rgb(54, 162, 235)", + ], + borderWidth: 1, + }, ], - borderWidth: 1 - }] - }, - options: { - scales: { - yAxes: [{ - ticks: { - beginAtZero: true, - precision: 0, - stepSize: 1 - } - }] - } - } - }); - //]]> - </script> + }; + const config = { + type: "bar", + data: data, + options: { + legend: { display: false }, + scales: { + yAxes: [ + { + ticks: { + beginAtZero: true, + min: 0, + precision: 0, + }, + }, + ], + }, + }, + }; + const chart = new Chart(ctx, config); + }); + </script> + </div> + </div> + + <div class="flex_column flex_grow"> + <div class="rounded shadow flex_column gap-0"> + <h2 class="heading">Feedback</h2> + <ul class="list" role="list"> + <li th:if="${feedback.isEmpty()}">No feedback</li> + <li th:each="feedbackItem : ${feedback}"> + <p th:text="${feedbackItem.feedback}"></p> + <div th:if="${feedbackItem.rating != null}"> + <span + style="color: #ebb345" + th:each="i : ${#numbers.sequence(0, feedbackItem.rating)}" + class="fa-solid fa-star"></span> + </div> + </li> + </ul> + </div> + + <th:block + layout:replace="~{pagination :: pagination(page=${feedback}, size=3)}"></th:block> </div> </div> - </div> -</section> -</body> + </th:block> </html> diff --git a/src/main/resources/templates/home/index.html b/src/main/resources/templates/home/index.html index c0786acc19f909ffa73e2cb7998ae441748f63a0..a9e01c8ec13f0b90d2a3ecc1b314e25a15cc1cac 100644 --- a/src/main/resources/templates/home/index.html +++ b/src/main/resources/templates/home/index.html @@ -1,47 +1,21 @@ -<!-- - - 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:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}"> - -<body> - <section layout:fragment="content"> - - <div class="page-header"> - <h1>Home</h1> - </div> - - <th:block th:unless="${@thymeleafConfig.isTheDay()}"> - Welcome to the TU Delft queue system!<br><br> - </th:block> - - <th:block th:if="${@thymeleafConfig.isTheDay()}"> - Welcome to the TU Delft stack system! First one in is still first one out, have a nice day.<br><br> - </th:block> - - - <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 - 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. - </section> -</body> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <div layout:fragment="content"> + <h1 class="title margin_bottom-3">Welcome to Queue!</h1> + + <h2 class="subtitle">How to use this system?</h2> + <p> + Click 'Log in' in the top right to log in. After you log in, you can enrol for a course. + Once enrolled you can navigate to a session. If the session is opened, you can enqueue + for an 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. + </p> + </div> </html> diff --git a/src/main/resources/templates/home/privacy.html b/src/main/resources/templates/home/privacy.html index c91e75064a404f53bc958e4f4d215fce0fc2080b..c43f8d2c528b51c4a4120eee818fb08962b93d2b 100644 --- a/src/main/resources/templates/home/privacy.html +++ b/src/main/resources/templates/home/privacy.html @@ -18,128 +18,204 @@ --> <!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}"> + <div layout:fragment="breadcrumbs" class="hidden"></div> -<body> -<section layout:fragment="content"> - <div class="page-header"> - <h1>Privacy statement</h1> - </div> - <p><strong>This privacy statement is a first version. Some aspects in this privacy statement might be changed in the - future to be in line with the law and best practices</strong></p> - - <p>The Queue project is a project from the TU Delft. In this privacy statement, it will be explained what personal data - the Queue collects when using the website. In this document the term Labrador will also be mentioned. Labrador is a - future project of the TU Delft, which will include the Queue.</p> - <h2 id="topics">Topics</h2> - <ul> - <li>What data do we collect?</li> - <li>How do we collect your data?</li> - <li>How will we use your data?</li> - <li>How will we store your data?</li> - <li>Other matters</li> - </ul> - <h2 id="what-data-do-we-collect">What data do we collect</h2> - <p>To support your education at the <th:block th:text="${@instituteProperties.name}"></th:block>, - the university has a legal ground to store your data. To be more specific, it is a contractual necessity: the data - processing is necessary for the performance of the contract between the student and - <th:block th:text="${@instituteProperties.name}"></th:block>. - Because of this, the Queue collects the following data:</p> - <ul> - <li>Name</li> - <li>Email</li> - <li>Student number</li> - <li>NetID</li> - <li>affiliation (if the user is a student or teacher)</li> - <li>which department/faculty the user is part of</li> - <li>all notifications the user has received</li> - </ul> - <p>For the purpose of providing notifications, we collect some metadata about the device used.</p> - <p>Furthermore, there exist multiple subgroups in the system. We will describe for each subgroup the extra data we - store.</p> - <h3 id="student">Student</h3> - <p>We collect the following information about students:</p> - <ul> - <li>The mentor group they are enrolled in</li> - <li>Courses they are enrolled in</li> - <li>The groups the student is/was part of</li> - </ul> - <p>We also collect every request the student makes (asked a question or asked to be signed off). This contains the - following items:</p> - <ul> - <li>The course the student is following</li> - <li>The lab the student is participating in</li> - <li>The assignment the student wants to hand in or has a question about</li> - <li>The room they are sitting in</li> - <li>The comment the student made on the request</li> - <li>The time the request was made</li> - <li>The timeslot the user has chosen if applicable</li> - <li>The question the student asked</li> - <li>The feedback on the the TA the student has written down</li> - </ul> - <h3 id="ta">TA</h3> - <p>For a TA (teaching assistant), we collect the following data:</p> - <ul> - <li>The requests handled by the TA</li> - <li>The time the request was assigned</li> - <li>The time the request was handled by the TA</li> - </ul> - <p>This data then enables us to see at which labs the TA attended.</p> - <p>Furthermore, feedback about the TA is stored.</p> - <h3 id="teacher">Teacher</h3> - <p>For a teacher, we collect the following data:</p> - <ul> - <li>all courses the teacher teaches</li> - </ul> - <h2 id="how-do-we-collect-your-data">How do we collect your data</h2> - <p>All data we collect is collected when using the Queue website.</p> - - <h2 id="how-will-we-use-your-data">How will we use your data</h2> - <p>At this moment, we only use the data given to support the functionality the Queue promises, giving students an easy - way to ask questions and sign off work and giving teaching staff an easy to use system to support signing off and - answering questions.</p> - <p>Data is being used on a need to know basis. This means that student-assistants only see, what they need to see to - fulfil their duties as teaching assistant and the same for teachers. Only administrators have access to the - database.</p> - <p>For example, student assistants cannot view each others feedback. Also teaching assistants can only see the name of - the student, not the NetID, email or student number of the student.</p> - <p>A teacher might export data to use for purposes to calculate final grades or import it into other tools, namely - BrightSpace and Osiris. Furthermore, admins are able to run a script to gather aggregate data about lab sessions and - specific people, who spent a lot of time with a teaching assistant. If these numbers are a cause for concern, they - will be relayed to the study advisor.</p> - <p>Teachers will look at the feedback of TA's to evaluate their performance and if action needs to be taken. The teacher - is able to determine the student that posted the feedback and is allowed to contact the student to inquire more - information.</p> - <p>Data from the Queue is is also used for research purposes. This research is done to explore the possibilities of - increasing efficiency and quality of the support given to students and teachers. Questions students asked will be - stored and used to figure out if there is a way to be able to answer easy questions right from the Queue, without a - need for a TA.</p> - <p>For this research the following data will be stored:</p> - <ul> - <li>The user id of the user that asked the question. This will help us to determine if a user asked a question - multiple times and if there is a statistically relevant correlation between multiple types of questions. We are - storing the user id as we do not need the name of the student</li> - <li>The content of the question. This content will be manually sorted through to remove all data that contains - revealing information about the user or a student assistant. For instance: "I'm the one with the red hair" or - "Yell Jack when looking for me as John, the previous TA couldn't find me"</li> - </ul> - <p>This data will only be accessible to people doing the experiment and will be responsibly handled in such a way that - no unauthorised person or persons can get access to this data.</p> - <h2 id="how-do-we-store-your-data">How do we store your data</h2> - <p>To comply with GDPR regulations, all data is stored on site at the <th:block th:text="${@instituteProperties.name}"></th:block>. - The data stored on our servers will be deleted after 5 years.</p> - <p>Access to data is strictly regulated with only select admins having access to the database. All other entities need - to use the website itself, where, as mentioned previously, information is provided on a need to know basis.</p> - <h1 id="other-matters">Other matters</h1> - <p>For matters like:</p> - <ul> - <li>What are your data protection rights</li> - <li>Which cookies we set and why</li> - </ul> - <p>Please refer to the <a href="https://www.tudelft.nl/privacy-statement/" - title="https://www.tudelft.nl/privacy-statement/">Privacy Statement of the TU Delft</a></p> - <p>If you have any questions about the privacy policy or want to make use of your rights, please contact <a - href="mailto:privacy-tud@tudelft.nl" title="mailto:privacy-tud@tudelft.nl">the following email address</a></p> -</section> -</body> + <section layout:fragment="content"> + <h1 class="title margin_bottom-3">Privacy statement</h1> + <p class="margin_bottom-3"> + <strong> + This privacy statement is a first version. Some aspects in this privacy statement + might be changed in the future to be in line with the law and best practices + </strong> + </p> + <p class="margin_bottom-5"> + The Queue project is a project from the TU Delft. In this privacy statement, it will be + explained what personal data the Queue collects when using the website. In this document + the term Labrador will also be mentioned. Labrador is a future project of the TU Delft, + which will include the Queue. + </p> + + <h2 class="subtitle" id="topics">Topics</h2> + <p class="margin_bottom-3">This privacy statement covers the following topics:</p> + <ul class="enumeration margin_bottom-5"> + <li>What data do we collect?</li> + <li>How do we collect your data?</li> + <li>How will we use your data?</li> + <li>How will we store your data?</li> + <li>Other matters</li> + </ul> + + <h2 class="subtitle" id="what-data-do-we-collect">What data do we collect</h2> + <p class="margin_bottom-3"> + To support your education at the + <th:block th:text="${@instituteProperties.name}"></th:block> + , the university has a legal ground to store your data. To be more specific, it is a + contractual necessity: the data processing is necessary for the performance of the + contract between the student and + <th:block th:text="${@instituteProperties.name}"></th:block> + . Because of this, the Queue collects the following data: + </p> + <ul class="enumeration margin_bottom-3"> + <li>Name</li> + <li>Email</li> + <li>Student number</li> + <li>NetID</li> + <li>affiliation (if the user is a student or teacher)</li> + <li>which department/faculty the user is part of</li> + <li>all notifications the user has received</li> + </ul> + <p> + For the purpose of providing notifications, we collect some metadata about the device + used. + </p> + <p class="margin_bottom-5"> + Furthermore, there exist multiple subgroups in the system. We will describe for each + subgroup the extra data we store. + </p> + + <h3 class="subsubtitle" id="student">Student</h3> + <p class="margin_bottom-3">We collect the following information about students:</p> + <ul class="enumeration margin_bottom-3"> + <li>The mentor group they are enrolled in</li> + <li>Courses they are enrolled in</li> + <li>The groups the student is/was part of</li> + </ul> + <p class="margin_bottom-3"> + We also collect every request the student makes (asked a question or asked to be signed + off). This contains the following items: + </p> + <ul class="enumeration margin_bottom-5"> + <li>The course the student is following</li> + <li>The lab the student is participating in</li> + <li>The assignment the student wants to hand in or has a question about</li> + <li>The room they are sitting in</li> + <li>The comment the student made on the request</li> + <li>The time the request was made</li> + <li>The timeslot the user has chosen if applicable</li> + <li>The question the student asked</li> + <li>The feedback on the the TA the student has written down</li> + </ul> + + <h3 class="subsubtitle" id="ta">TA</h3> + <p class="margin_bottom-3">For a TA (teaching assistant), we collect the following data:</p> + <ul class="enumeration margin_bottom-3"> + <li>The requests handled by the TA</li> + <li>The time the request was assigned</li> + <li>The time the request was handled by the TA</li> + </ul> + <p class="margin_bottom-3"> + This data then enables us to see at which labs the TA attended. + </p> + <p class="margin_bottom-5">Furthermore, feedback about the TA is stored.</p> + + <h3 class="subsubtitle" id="teacher">Teacher</h3> + <p class="margin_bottom-3">For a teacher, we collect the following data:</p> + <ul class="enumeration margin_bottom-5"> + <li>all courses the teacher teaches</li> + </ul> + + <h2 class="subtitle" id="how-do-we-collect-your-data">How do we collect your data</h2> + <p class="margin_bottom-5"> + All data we collect is collected when using the Queue website. + </p> + + <h2 class="subtitle" id="how-will-we-use-your-data">How will we use your data</h2> + <p class="margin_bottom-5"> + At this moment, we only use the data given to support the functionality the Queue + promises, giving students an easy way to ask questions and sign off work and giving + teaching staff an easy to use system to support signing off and answering questions. + </p> + <p class="margin_bottom-5"> + Data is being used on a need to know basis. This means that student-assistants only see, + what they need to see to fulfil their duties as teaching assistant and the same for + teachers. Only administrators have access to the database. + </p> + <p class="margin_bottom-5"> + For example, student assistants cannot view each others feedback. Also teaching + assistants can only see the name of the student, not the NetID, email or student number + of the student. + </p> + <p class="margin_bottom-5"> + A teacher might export data to use for purposes to calculate final grades or import it + into other tools, namely BrightSpace and Osiris. Furthermore, admins are able to run a + script to gather aggregate data about lab sessions and specific people, who spent a lot + of time with a teaching assistant. If these numbers are a cause for concern, they will + be relayed to the study advisor. + </p> + <p class="margin_bottom-5"> + Teachers will look at the feedback of TA's to evaluate their performance and if action + needs to be taken. The teacher is able to determine the student that posted the feedback + and is allowed to contact the student to inquire more information. + </p> + <p class="margin_bottom-5"> + Data from the Queue is is also used for research purposes. This research is done to + explore the possibilities of increasing efficiency and quality of the support given to + students and teachers. Questions students asked will be stored and used to figure out if + there is a way to be able to answer easy questions right from the Queue, without a need + for a TA. + </p> + <p class="margin_bottom-5">For this research the following data will be stored:</p> + <ul class="enumeration margin_bottom-3"> + <li> + The user id of the user that asked the question. This will help us to determine if a + user asked a question multiple times and if there is a statistically relevant + correlation between multiple types of questions. We are storing the user id as we do + not need the name of the student + </li> + <li> + The content of the question. This content will be manually sorted through to remove + all data that contains revealing information about the user or a student assistant. + For instance: "I'm the one with the red hair" or "Yell Jack when looking for me as + John, the previous TA couldn't find me" + </li> + </ul> + <p class="margin_bottom-5"> + This data will only be accessible to people doing the experiment and will be responsibly + handled in such a way that no unauthorised person or persons can get access to this + data. + </p> + + <h2 class="subtitle" id="how-do-we-store-your-data">How do we store your data</h2> + <p class="margin_bottom-3"> + To comply with GDPR regulations, all data is stored on site at the + <th:block th:text="${@instituteProperties.name}"></th:block> + . The data stored on our servers will be deleted after 5 years. + </p> + <p class="margin_bottom-7"> + Access to data is strictly regulated with only select admins having access to the + database. All other entities need to use the website itself, where, as mentioned + previously, information is provided on a need to know basis. + </p> + + <h1 class="title margin_bottom-3" id="other-matters">Other matters</h1> + <p class="margin_bottom-3">For matters like:</p> + <ul class="enumeration margin_bottom-3"> + <li>What are your data protection rights</li> + <li>Which cookies we set and why</li> + </ul> + <p class="margin_bottom-3"> + Please refer to the + <a + class="link" + href="https://www.tudelft.nl/privacy-statement/" + title="https://www.tudelft.nl/privacy-statement/"> + Privacy Statement of the TU Delft + </a> + </p> + <p> + If you have any questions about the privacy policy or want to make use of your rights, + please contact + <a + class="link" + href="mailto:privacy-tud@tudelft.nl" + title="mailto:privacy-tud@tudelft.nl"> + the following email address + </a> + </p> + </section> </html> diff --git a/src/main/resources/templates/lab/create.html b/src/main/resources/templates/lab/create.html index e71d7e3589deb060f76be1f7540100b893c205d0..57ac0313e133f952a8b93c760cdedabd0fa484fd 100644 --- a/src/main/resources/templates/lab/create.html +++ b/src/main/resources/templates/lab/create.html @@ -1,73 +1,134 @@ -<!-- - - 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}"> -<head> - <title th:text="|Create Lab for ${'#' + (ec == null ? edition.id : ec.id)}|"></title> - - <script src="/webjars/momentjs/min/moment.min.js"></script> - - <script type="application/javascript" - src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script> - <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/> - - <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script> - <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/> - - <link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css"> - <script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script> -</head> - -<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"--> -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> - -<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.labs.LabCreateDTO"--> -<!--@thymesVar id="lType" type="nl.tudelft.queue.model.enums.QueueSessionType"--> - -<body> -<section layout:fragment="subcontent"> - <div class="page-header"> - <h3>Create lab</h3> - </div> - <form th:with="action = @{/{ou}/{id}/lab/create/{lTy}(id=${ec == null ? edition.id : ec.id}, ou=${ec == null ? 'edition' : 'shared-edition'}, lTy=${lType.name().toLowerCase()})}" - th:action="${action}" th:object="${dto}" - class="form-horizontal" method="post" id="form"> - - <section layout:fragment="general-config"></section> - - <section layout:fragment="slot-config"></section> - <section layout:fragment="exam-config"></section> - <section layout:fragment="capacity-config"></section> - - <section layout:fragment="advanced-config"></section> - - <div class="form-group form-row"> - <div class="col-sm-offset-2 col-sm-8"> - <button type="submit" class="btn btn-primary ctrl-enter-submit"> - Create new lab +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog th:id="create-session" layout:fragment="dialog(shared=false)"> + <div class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + data-create-session-cancel + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <button + data-create-session-prev + class="hidden:from_small fa-solid fa-arrow-left hidden" + type="button"></button> + <h2> + Create session - + <span id="create-session-part">Type</span> + </h2> + </div> + <button data-create-session-next class="hidden:from_small" type="button"> + Next + </button> + <button data-create-session-create class="hidden:from_small hidden"> + <span data-role="small_label">Create</span> </button> - <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> + </div> + + <div class="dialog__body"> + <nav + id="create-session-nav" + class="hidden nav_list margin_bottom-5" + style="--align: center" + data-style="outlined" + data-direction="horizontal"></nav> + + <input type="hidden" id="create-session-id" name="id" /> + + <th:block layout:replace="~{lab/create/type :: form}"></th:block> + <th:block layout:replace="~{lab/create/general :: form}"></th:block> + <th:block layout:replace="~{lab/create/rooms :: form}"></th:block> + <th:block layout:replace="~{lab/create/modules :: form}"></th:block> + <th:block layout:replace="~{lab/create/assignments :: form}"></th:block> + <th:block layout:replace="~{lab/create/allowed_requests :: form}"></th:block> + <th:block layout:replace="~{lab/create/limited_capacity :: form}"></th:block> + <th:block layout:replace="~{lab/create/slots :: form}"></th:block> + <th:block layout:replace="~{lab/create/exam :: form}"></th:block> + <th:block layout:replace="~{lab/create/extra_settings :: settings}"></th:block> + + <div class="flex space_between span-2 margin_top-7 | hidden:small"> + <button + data-create-session-cancel + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + data-create-session-prev + class="button padding_inline-5 padding_block-2 hidden" + type="button" + data-style="outlined"> + <span class="margin_right-1 fa-solid fa-arrow-left"></span> + <span>Back</span> + </button> + <button + data-create-session-next + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined"> + <span class="margin_right-1 fa-solid fa-arrow-right"></span> + <span>Next</span> + </button> + <button + data-create-session-create + class="button padding_inline-5 padding_block-2 hidden" + type="submit" + data-style="outlined" + th:data-organisational-unit="${edition?.id} ?: ${collection.id}" + th:data-shared="${shared == true}"> + <span data-role="icon" class="margin_right-1 fa-solid fa-plus"></span> + <span data-role="label">Create session</span> + </button> + </div> </div> </div> - </form> -</section> -</body> + </dialog> + + <th:block layout:fragment="create_script"> + <script src="/js/session_wizard.js"></script> + <script> + $(() => { + $("#create-session-type").change(function () { + sessionType = $(this).val(); + }); + + $("[data-create-session-cancel]").click(clearForms); + + $("[data-create-session-prev]").click(() => goToForm(getPrevForm())); + + const forms = $("[data-create-session-part]"); + setValidityCheck(forms.find(":where(input, select, textarea)")); + + /* + * On click next, check validity of form + * If valid, go to next + * If not, report what is wrong to the user + */ + $("[data-create-session-next]").click(function () { + $(`[data-create-session-part="${currentForm}"]`).submit(); + }); + forms.submit(function (event) { + event.preventDefault(); + + if (!isValid($(this))) { + reportValid($(this)); + return; + } + if (currentForm === "type") createNav(); + goToForm(getNextForm()); + }); + + $("[data-create-session-create]").click(function () { + submitSession($(this).data("organisationalUnit"), $(this).data("shared")); + }); + }); + </script> + </th:block> </html> diff --git a/src/main/resources/templates/lab/create/allowed_requests.html b/src/main/resources/templates/lab/create/allowed_requests.html new file mode 100644 index 0000000000000000000000000000000000000000..1c5fd59a6fe66c52994329fb2b24880519ac2488 --- /dev/null +++ b/src/main/resources/templates/lab/create/allowed_requests.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="allowed_requests" + class="flex_column hidden" + layout:fragment="form"> + <script> + $(() => { + let defer = $.Deferred(); + + $("[data-create-session-part='allowed_requests']") + .on("show", function () { + let aIds = Object.keys(assignments); + const compareStrings = (a, b) => (a === b ? 0 : a < b ? -1 : 1); + aIds.sort((a, b) => + compareStrings(assignments[a].name, assignments[b].name) + ); + aIds.filter(id => assignments[id].selected) + .filter(a => $(`#create-session-requests-${a}-group`).length === 0) + .forEach(a => { + const select = + $(`<select id="create-session-requests-${a}" name="${a}:requestTypes" data-placeholder="Select requests" data-select multiple data-style="outlined" required> + <option value="QUESTION">Question</option> + <option value="SUBMISSION">Submisson</option> + </select>`); + + const label = $( + `<label for="create-session-requests-${a}" class="form__label">Allowed requests for ${assignments[a].name}</label>` + ); + const group = $( + `<div id="create-session-requests-${a}-group" class="flex_column gap-1" data-remove-on-cancel></div>` + ); + group.append(label).append(select); + + configureSelect(select); + + $(this).append(group); + setValidityCheck(select); + select.change(); + }); + aIds.filter(id => !assignments[id].selected).forEach(a => + $(`#create-session-requests-${a}-group`).remove() + ); + defer.resolve(); + }) + .on("edit", function (event, session) { + defer = $.Deferred(); + defer.then(() => { + const aIds = Object.keys(assignments); + const requests = {}; + aIds.forEach(a => { + requests[a] = []; + }); + session.allowedRequests.forEach(r => { + requests[r.assignment].push(r.type); + }); + aIds.filter(id => assignments[id].selected).forEach(a => { + $(`#create-session-requests-${a}`) + .val(requests[a]) + .change() + .each((i, sel) => updateSelect($(sel))); + }); + }); + assignmentsLoaded.then(() => $(this).trigger("show")); + }); + }); + </script> + </form> +</html> diff --git a/src/main/resources/templates/lab/create/assignments.html b/src/main/resources/templates/lab/create/assignments.html new file mode 100644 index 0000000000000000000000000000000000000000..f921197b3a7dc3a0948551d578068bc03f891aa6 --- /dev/null +++ b/src/main/resources/templates/lab/create/assignments.html @@ -0,0 +1,136 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="assignments" + class="flex_column hidden" + layout:fragment="form"> + <div class="flex_column gap-1"> + <label for="create-session-modules" class="form__label">Modules</label> + <select + id="create-session-modules" + name="modules" + data-placeholder="Select modules" + data-select + multiple + data-style="outlined" + required + autocomplete="off"> + <option + th:each="module : ${allModules}" + th:value="${module.id}" + th:text="${module.name}"></option> + </select> + </div> + + <script> + let assignments = {}; + let divisions = {}; + let assignmentsLoaded = $.Deferred(); + + $(() => { + let modules = []; + const form = $("[data-create-session-part='assignments']"); + + $("#create-session-modules").change(function () { + const newModules = $(this).val(); + if (modules.length > newModules.length) { + // module deselected + modules + .filter(m => !newModules.includes(m)) + .forEach(m => { + $(`#create-session-assignments-${m}-group`).remove(); + delete divisions[m]; + }); + modules = newModules; + } else { + // module selected + const promises = newModules + .filter(m => !modules.includes(m)) + .map(m => { + return fetch(`/module/${m}`) + .then(res => res.json()) + .then(module => { + const select = $( + `<select id="create-session-assignments-${m}" name="assignments-${m}" data-placeholder="Select assignments" data-select multiple data-style="outlined" required></select>` + ); + const label = $( + `<label for="create-session-assignments-${m}" class="form__label">Assignments in ${module.name}</label>` + ); + + divisions[m] = { + name: module.name, + divisions: module.divisions, + }; + module.assignments.forEach(assignment => { + assignments[assignment.id] = { + name: `${module.name} - ${assignment.name}`, + module: module.id, + }; + select.append( + `<option value="${assignment.id}">${assignment.name}</option>` + ); + }); + + const group = $( + `<div id="create-session-assignments-${m}-group" class="flex_column gap-1" data-remove-on-cancel></div>` + ); + group.append(label).append(select); + configureSelect(select); + + select.change(function () { + Object.keys(assignments) + .filter(a => assignments[a].module === module.id) + .forEach( + id => + (assignments[id].selected = $(this) + .val() + .includes(id)) + ); + }); + + form.append(group); + modules = newModules; + + setValidityCheck(select); + select.change(); + }) + .catch(() => alert("Error fetching assignments for module")); + }); + Promise.all(promises).then(() => assignmentsLoaded.resolve()); + } + }); + + form.on("edit", function (event, session) { + const moduleIds = session.modules.map(m => m.id); + const modules = {}; + session.modules.forEach(m => (modules[m.id] = m)); + + assignmentsLoaded = $.Deferred(); + assignmentsLoaded.then(() => { + moduleIds.forEach(m => + $(`#create-session-assignments-${m}`) + .val( + session.session.assignments + .filter(a => + modules[m].assignments.map(ma => ma.id).includes(a.id) + ) + .map(a => a.id) + ) + .change() + .each((i, sel) => updateSelect($(sel))) + ); + }); + + $("#create-session-modules") + .val(moduleIds) + .each((i, sel) => updateSelect($(sel))) + .change(); + }); + }); + </script> + </form> +</html> diff --git a/src/main/resources/templates/lab/create/exam.html b/src/main/resources/templates/lab/create/exam.html index cbee1acae5416603bfc85a0ae1e19f2f6fa33573..701da965221157166f823551fabe304937237062 100644 --- a/src/main/resources/templates/lab/create/exam.html +++ b/src/main/resources/templates/lab/create/exam.html @@ -1,55 +1,34 @@ -<!-- - - 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="~{lab/create/slotted}"> -<head> - <title>Create Exam Lab</title> -</head> - -<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"--> -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> - -<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"--> -<!--@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="dto" type="nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO"--> -<!--@thymesVar id="lType" type="nl.tudelft.queue.model.enums.QueueSessionType"--> - -<body> -<th:block th:object="${dto}"> - <section layout:fragment="exam-config"> - <h4>Exam</h4> - <hr/> - - <div class="form-group form-row"> - <label for="exam-percentage-input" class="col-sm-2 col-form-label">Percentage for Exam lab</label> - <div class="col-sm-4"> - <input id="exam-percentage-input" th:field="*{examLabConfig.percentage}" - min="10" max="100" type="number" placeholder="15" class="form-control" - required/> - </div> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="exam" + class="flex_column hidden" + layout:fragment="form"> + <div class="flex_column gap-1"> + <label for="create-session-exam-percentage" class="form__label"> + Percentage for Exam Lab + </label> + <input + class="textfield" + type="number" + id="create-session-exam-percentage" + name="examLabConfig.percentage" + data-style="outlined" + value="15" + min="0" + required /> </div> - </section> -</th:block> -</body> + <script> + $(() => { + $("[data-create-session-part='slots']").on("edit", function (event, session) { + $("#create-session-exam-percentage").val(session.examLabConfig.percentage); + }); + }); + </script> + </form> </html> diff --git a/src/main/resources/templates/lab/create/extra_settings.html b/src/main/resources/templates/lab/create/extra_settings.html new file mode 100644 index 0000000000000000000000000000000000000000..bfca39db535457a547e13fec62f2e2e4c401dade --- /dev/null +++ b/src/main/resources/templates/lab/create/extra_settings.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="extra_settings" + class="flex hidden | flex_column:small" + layout:fragment="settings"> + <nav data-tabs aria-label="Extra settings"> + <ul + class="nav_list" + style="--align: center; --width: 12rem" + role="list" + data-style="outlined"> + <li data-active="true" th:data-view="create-session-extra-info"> + <button type="button">Extra info</button> + </li> + <li data-active="false" th:data-view="create-session-session-timing"> + <button type="button">Session timing</button> + </li> + <li data-active="false" th:data-view="create-session-experimental"> + <button type="button">Experimental</button> + </li> + <li data-active="false" th:data-view="create-session-request-constraints"> + <button type="button">Request constraints</button> + </li> + <li + data-active="false" + th:data-view="create-session-repetition" + data-create-session-wizard="create,copy"> + <button type="button">Repetition</button> + </li> + </ul> + </nav> + + <th:block layout:replace="~{lab/create/extra_settings/extra_info :: form}"></th:block> + <th:block layout:replace="~{lab/create/extra_settings/session_timing :: form}"></th:block> + <th:block layout:replace="~{lab/create/extra_settings/experimental :: form}"></th:block> + <th:block + layout:replace="~{lab/create/extra_settings/request_constraints :: form}"></th:block> + <th:block layout:replace="~{lab/create/extra_settings/repetition :: form}"></th:block> + </form> +</html> diff --git a/src/main/resources/templates/lab/create/extra_settings/experimental.html b/src/main/resources/templates/lab/create/extra_settings/experimental.html new file mode 100644 index 0000000000000000000000000000000000000000..d70268b10ffe5e0444eb2b6e03dcdcb84a53be6a --- /dev/null +++ b/src/main/resources/templates/lab/create/extra_settings/experimental.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div + layout:fragment="form" + id="create-session-experimental" + class="hidden flex_column flex_grow"> + <div> + <input + type="checkbox" + id="create-session-experimental-checkbox" + name="enableExperimental" + value="true" /> + <label for="create-session-experimental-checkbox" class="font-400"> + Enable experimental features + </label> + </div> + + <script> + $(() => { + $("[data-create-session-part='extra_settings']").on( + "edit", + function (event, session) { + $("#create-session-experimental-checkbox").text(session.enableExperimental); + } + ); + }); + </script> + </div> +</html> diff --git a/src/main/resources/templates/lab/create/extra_settings/extra_info.html b/src/main/resources/templates/lab/create/extra_settings/extra_info.html new file mode 100644 index 0000000000000000000000000000000000000000..d253aed01080a7e00bf3c0021a9fb6f1d6a55c84 --- /dev/null +++ b/src/main/resources/templates/lab/create/extra_settings/extra_info.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div layout:fragment="form" id="create-session-extra-info" class="flex_column flex_grow"> + <div class="flex_column gap-1"> + <label for="create-session-extra-info-field" class="form__label"> + Extra session info + </label> + <textarea + id="create-session-extra-info-field" + name="extraInfo" + class="textfield" + data-style="outlined" + style="resize: none" + rows="5"></textarea> + </div> + + <script> + $(() => { + $("[data-create-session-part='extra_settings']").on( + "edit", + function (event, session) { + $("#create-session-extra-info-field").html(session.extraInfo); + } + ); + }); + </script> + </div> +</html> diff --git a/src/main/resources/templates/lab/create/extra_settings/repetition.html b/src/main/resources/templates/lab/create/extra_settings/repetition.html new file mode 100644 index 0000000000000000000000000000000000000000..5c977a7ac68a731ac005c3813e5f72235c47351b --- /dev/null +++ b/src/main/resources/templates/lab/create/extra_settings/repetition.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div + layout:fragment="form" + id="create-session-repetition" + class="flex_column hidden flex_grow" + data-create-session-wizard="create,copy"> + <div class="flex_column gap-1"> + <label for="create-session-repeat" class="form__label"> + Number of weeks to repeat this session + </label> + <input + type="number" + id="create-session-repeat" + name="repeatForXWeeks" + class="textfield" + data-style="outlined" + placeholder="0" /> + </div> + </div> +</html> diff --git a/src/main/resources/templates/lab/create/extra_settings/request_constraints.html b/src/main/resources/templates/lab/create/extra_settings/request_constraints.html new file mode 100644 index 0000000000000000000000000000000000000000..441c1f2123348b1153b943b2c30400bb4398b830 --- /dev/null +++ b/src/main/resources/templates/lab/create/extra_settings/request_constraints.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div + layout:fragment="form" + id="create-session-request-constraints" + class="flex_column hidden flex_grow"> + <div class="flex_column gap-1"> + <label for="create-session-cluster-constraint" class="form__label"> + Cluster restriction + </label> + <select + id="create-session-cluster-constraint" + name="constraints.clusterConstaint.clusters" + data-select + multiple + data-style="outlined" + data-placeholder="All clusters" + autocomplete="off"> + <option + th:each="cluster : ${clusters}" + th:value="${cluster.id}" + th:text="${cluster.name}"></option> + </select> + </div> + + <script> + $(() => { + $("[data-create-session-part='extra_settings']") + .on("show", () => { + let mIds = Object.keys(divisions); + const compareStrings = (a, b) => (a === b ? 0 : a < b ? -1 : 1); + mIds.sort((a, b) => compareStrings(divisions[a].name, divisions[b].name)); + + $("#create-session-division-constraint-group").remove(); + const select = $( + `<select id="create-session-division-constraint" name="constraints.moduleDivisionConstraint.divisions" data-placeholder="All disivions" data-select multiple data-style="outlined"></select>` + ); + const label = $( + `<label for="create-session-division-constraint" class="form__label">Division restriction</label>` + ); + + mIds.forEach(m => + divisions[m].divisions.forEach(md => { + select.append( + $( + `<option value="${md.id}">${divisions[m].name} - Division #${md.id}</option>` + ) + ); + }) + ); + + const group = $( + `<div id="create-session-division-constraint-group" class="flex_column gap-1"></div>` + ); + group.append(label).append(select); + configureSelect(select); + + $("#create-session-request-constraints").append(group); + }) + .on("edit", function (event, session) { + assignmentsLoaded.then(() => $(this).trigger("show")); + }); + }); + </script> + </div> +</html> diff --git a/src/main/resources/templates/lab/create/extra_settings/session_timing.html b/src/main/resources/templates/lab/create/extra_settings/session_timing.html new file mode 100644 index 0000000000000000000000000000000000000000..9db51ecbacbaa055cdf9a3dbac1f30f56f6284ce --- /dev/null +++ b/src/main/resources/templates/lab/create/extra_settings/session_timing.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div + layout:fragment="form" + id="create-session-session-timing" + class="flex_column hidden flex_grow"> + <div class="flex_column gap-1"> + <label for="create-session-grace-period" class="form__label"> + End of session grace period + </label> + <input + type="number" + min="0" + value="15" + id="create-session-grace-period" + name="eolGracePeriod" + class="textfield" + data-style="outlined" + required + data-initial="15" /> + </div> + + <script> + $(() => { + $("[data-create-session-part='extra_settings']").on( + "edit", + function (event, session) { + $("#create-session-grace-period").val(session.eolGracePeriod); + } + ); + }); + </script> + </div> +</html> diff --git a/src/main/resources/templates/lab/create/general.html b/src/main/resources/templates/lab/create/general.html new file mode 100644 index 0000000000000000000000000000000000000000..71e59d88494830cb8b53fed489b825c03320260a --- /dev/null +++ b/src/main/resources/templates/lab/create/general.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="general" + class="flex_column hidden" + layout:fragment="form"> + <div class="flex_column gap-1"> + <label for="create-session-name" class="form__label">Name</label> + <input + id="create-session-name" + name="name" + class="textfield" + placeholder="Session name" + data-style="outlined" + required /> + </div> + + <div class="flex_column gap-1"> + <label for="create-session-direction" class="form__label">Direction</label> + <select + id="create-session-direction" + name="communicationMethod" + data-placeholder="Pick a direction" + data-select + data-style="outlined" + required + autocomplete="off"> + <option data-empty-option value=""></option> + <option value="STUDENT_VISIT_TA">Student visits TA</option> + <option value="TA_VISIT_STUDENT">TA visits student</option> + <option value="JITSI_MEET">Jitsi meeting</option> + </select> + </div> + + <div class="flex_column gap-1"> + <label for="create-session-start" class="form__label">Session start</label> + <input + class="textfield" + type="datetime-local" + id="create-session-start" + name="slot.opensAt" + data-style="outlined" + required /> + </div> + + <div class="flex_column gap-1"> + <label for="create-session-end" class="form__label">Session end</label> + <input + class="textfield" + type="datetime-local" + id="create-session-end" + name="slot.closesAt" + data-style="outlined" + required /> + </div> + + <script> + $(() => { + const format = "YYYY-MM-DDTHH:mm"; + $("#create-session-start") + .focus(function () { + if ($(this).val() === "") { + $(this).val(moment().format(format)).change(); + } + }) + .change(function () { + const end = $("#create-session-end"); + if (end.data("set") !== true) { + end.val( + moment($(this).val(), format) + .add(3, "hours") + .add(45, "minutes") + .format(format) + ); + } + }); + $("#create-session-end").change(function () { + $(this).data("set", true); + }); + $("[data-create-session-part='general']") + .on("clear", function () { + $("#create-session-end").data("set", false); + }) + .on("edit", function (event, session) { + $("#create-session-name").val(session.session.name); + $("#create-session-direction") + .val(session.communicationMethod) + .each((i, sel) => updateSelect($(sel))); + $("#create-session-start").val( + moment(session.session.start).format(format) + ); + $("#create-session-end").val(moment(session.session.end).format(format)); + }); + }); + </script> + </form> +</html> diff --git a/src/main/resources/templates/lab/create/limited_capacity.html b/src/main/resources/templates/lab/create/limited_capacity.html new file mode 100644 index 0000000000000000000000000000000000000000..fa2779e875b27b80183b922ae29580574ede62d1 --- /dev/null +++ b/src/main/resources/templates/lab/create/limited_capacity.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="limited_capacity" + class="flex_column hidden" + layout:fragment="form"> + <div class="flex_column gap-1"> + <label for="create-session-procedure" class="form__label">Selection procedure</label> + <select + id="create-session-procedure" + name="capacitySessionConfig.procedure" + data-style="outlined" + required + data-select + data-placeholder="Select a procedure"> + <option data-empty-option value=""></option> + <option + th:each="procedure : ${T(nl.tudelft.queue.model.enums.SelectionProcedure).values()}" + th:value="${procedure}" + th:text="#{|session.selection.procedure.${procedure.name().toLowerCase()}|}"></option> + </select> + </div> + + <div class="flex_column gap-1"> + <label for="create-session-selection-enrol-open" class="form__label"> + Enrolment opening time + </label> + <input + class="textfield" + type="datetime-local" + id="create-session-selection-enrol-open" + name="capacitySessionConfig.enrolmentOpensAt" + data-style="outlined" + required /> + </div> + + <div class="flex_column gap-1"> + <label for="create-session-selection-enrol-open" class="form__label"> + Enrolment closing time + </label> + <input + class="textfield" + type="datetime-local" + id="create-session-selection-enrol-close" + name="capacitySessionConfig.enrolmentClosesAt" + data-style="outlined" + required /> + </div> + + <div class="flex_column gap-1"> + <label for="create-session-selection-enrol-open" class="form__label"> + Selection time + </label> + <input + class="textfield" + type="datetime-local" + id="create-session-selection-enrol-selection" + name="capacitySessionConfig.selectionAt" + data-style="outlined" + required /> + </div> + + <script> + $(() => { + const format = "YYYY-MM-DDTHH:mm"; + $("[data-create-session-part='limited_capacity']").on( + "edit", + function (event, session) { + $("#create-session-procedure").val(session.capacitySessionConfig.procedure); + $("#create-session-selection-enrol-open").val( + moment(session.capacitySessionConfig.enrolmentOpensAt).format(format) + ); + $("#create-session-selection-enrol-close").val( + moment(session.capacitySessionConfig.enrolmentClosesAt).format(format) + ); + $("#create-session-selection-enrol-selection").val( + moment(session.capacitySessionConfig.selectionAt).format(format) + ); + } + ); + }); + </script> + </form> +</html> diff --git a/src/main/resources/templates/lab/create/modules.html b/src/main/resources/templates/lab/create/modules.html new file mode 100644 index 0000000000000000000000000000000000000000..ac8c402ed45e32991b77d19612b1ff5984177937 --- /dev/null +++ b/src/main/resources/templates/lab/create/modules.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="modules" + class="flex_column hidden" + layout:fragment="form"> + <div class="flex_column gap-1"> + <label for="create-session-modules-only" class="form__label">Modules</label> + <select + id="create-session-modules-only" + name="modules" + data-placeholder="Select modules" + data-select + multiple + data-style="outlined" + required + autocomplete="off"> + <option + th:each="module : ${allModules}" + th:value="${module.id}" + th:text="${module.name}"></option> + </select> + </div> + + <script> + $(() => { + $("[data-create-session-part='modules']").on("edit", function (event, session) { + $("#create-session-modules-only").val(session.modules); + }); + }); + </script> + </form> +</html> diff --git a/src/main/resources/templates/lab/create/rooms.html b/src/main/resources/templates/lab/create/rooms.html new file mode 100644 index 0000000000000000000000000000000000000000..520d3726bba71107427a76d5cb2dc578fb5068a0 --- /dev/null +++ b/src/main/resources/templates/lab/create/rooms.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="rooms" + class="flex_column hidden" + layout:fragment="form"> + <div class="flex_column gap-1"> + <label for="create-session-buildings" class="form__label">Buildings</label> + <select + id="create-session-buildings" + name="buildings" + data-placeholder="Select buildings" + data-select + multiple + data-style="outlined" + required + autocomplete="off"> + <option + th:each="building : ${buildings}" + th:value="${building.id}" + th:text="${building.name}"></option> + </select> + </div> + + <script> + $(() => { + let buildings = []; + let defer = $.Deferred(); + const form = $("[data-create-session-part='rooms']"); + + $("#create-session-buildings").change(function () { + const newBuildings = $(this).val(); + if (buildings.length > newBuildings.length) { + // building deselected + buildings + .filter(b => !newBuildings.includes(b)) + .forEach(b => $(`#create-session-rooms-${b}-group`).remove()); + buildings = newBuildings; + } else { + // building selected + const promises = newBuildings + .filter(b => !buildings.includes(b)) + .map(b => { + return fetch(`/building/${b}`) + .then(res => res.json()) + .then(building => { + const select = $( + `<select id="create-session-rooms-${b}" name="rooms-${b}" data-placeholder="Select rooms" data-select multiple data-style="outlined" required></select>` + ); + const label = $( + `<label for="create-session-rooms-${b}" class="form__label">Rooms in ${building.name}</label>` + ); + building.rooms.forEach(room => { + select.append( + `<option value="${room.id}">${room.name}</option>` + ); + }); + const group = $( + `<div id="create-session-rooms-${b}-group" class="flex_column gap-1" data-remove-on-cancel></div>` + ); + group.append(label).append(select); + configureSelect(select); + form.append(group); + buildings = newBuildings; + + setValidityCheck(select); + select.change(); + }) + .catch(() => alert("Error fetching rooms for building")); + }); + Promise.all(promises).then(() => defer.resolve()); + } + }); + + form.on("edit", function (event, session) { + const buildings = [...new Set(session.session.rooms.map(r => r.building.id))]; + + defer = $.Deferred(); + defer.then(() => { + buildings.forEach(b => + $(`#create-session-rooms-${b}`) + .val( + session.session.rooms + .filter(r => r.building.id === b) + .map(r => r.id) + ) + .change() + .each((i, sel) => updateSelect($(sel))) + ); + }); + + $("#create-session-buildings") + .val(buildings) + .each((i, sel) => updateSelect($(sel))) + .change(); + }); + }); + </script> + </form> +</html> diff --git a/src/main/resources/templates/lab/create/slots.html b/src/main/resources/templates/lab/create/slots.html new file mode 100644 index 0000000000000000000000000000000000000000..d77d8e5dfa71f7938cb0be9e6bf6230e739a142d --- /dev/null +++ b/src/main/resources/templates/lab/create/slots.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="slots" + class="flex_column hidden" + layout:fragment="form"> + <div class="flex_column gap-1"> + <label for="create-session-interval" class="form__label"> + Interval length in minutes + </label> + <input + class="textfield" + type="number" + id="create-session-interval" + name="slottedLabConfig.duration" + data-style="outlined" + value="15" + min="1" + required /> + </div> + + <div class="flex_column gap-1"> + <label for="create-session-time-slot-capacity" class="form__label"> + Number of requests per time slot + </label> + <input + class="textfield" + type="number" + id="create-session-time-slot-capacity" + name="slottedLabConfig.capacity" + data-style="outlined" + value="3" + min="1" + required /> + </div> + + <div class="flex_column gap-1"> + <label for="create-session-slot-selection-time" class="form__label"> + Slot selection opening time + </label> + <input + class="textfield" + type="datetime-local" + id="create-session-slot-selection-time" + name="slottedLabConfig.selectionOpensAt" + data-style="outlined" + required /> + </div> + + <script> + $(() => { + const format = "YYYY-MM-DDTHH:mm"; + $("[data-create-session-part='slots']").on("edit", function (event, session) { + $("#create-session-interval").val(session.slottedLabConfig.duration); + $("#create-session-time-slot-capacity").val(session.slottedLabConfig.capacity); + $("#create-session-slot-selection-time").val( + moment(session.slottedLabConfig.selectionOpensAt).format(format) + ); + }); + }); + </script> + </form> +</html> diff --git a/src/main/resources/templates/lab/create/type.html b/src/main/resources/templates/lab/create/type.html new file mode 100644 index 0000000000000000000000000000000000000000..87a224f930e0d54965981d4330d1caacbf960355 --- /dev/null +++ b/src/main/resources/templates/lab/create/type.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <form + action="#" + data-create-session-part="type" + class="flex_column gap-1" + layout:fragment="form"> + <label for="create-session-type" class="form__label">Session type</label> + <select id="create-session-type" name="type" data-select data-style="outlined"> + <option selected th:value="REGULAR">Regular</option> + <option th:value="SLOTTED">Slotted</option> + <option th:value="EXAM">Exam</option> + <option th:value="CAPACITY">Limited capacity</option> + </select> + + <script> + $(() => { + $("[data-create-session-part='type']").on("edit", function (event, session) { + $("#create-session-type").val(session.type).change(); + $(`[data-create-session-part="${currentForm}"]`).submit(); + }); + }); + </script> + </form> +</html> diff --git a/src/main/resources/templates/lab/enqueue.html b/src/main/resources/templates/lab/enqueue.html index cc899c729411eb37f2fabb168fcefd5b4490da2c..a1b48073d09a8f337c65e5adec538dd90e697fee 100644 --- a/src/main/resources/templates/lab/enqueue.html +++ b/src/main/resources/templates/lab/enqueue.html @@ -1,66 +1,126 @@ -<!-- - - 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 di•••••••••••stributed 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="qSession" type="nl.tudelft.queue.model.QueueSession"--> -<!--@thymesVar id="request" type="nl.tudelft.queue.dto.create.RequestCreateDTO"--> - -<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"--> -<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"--> -<!--@thymesVar id="types" type="java.util.Map<java.lang.Long, java.util.Set<java.lang.String>>"--> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="enqueue" layout:fragment="dialog"> + <form + th:action="@{/lab/{lab}/enqueue/lab(lab=${qSession.id})}" + method="post" + class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Enqueue</h2> + </div> + <button class="hidden:from_small">Enqueue</button> + </div> -<!--@thymesVar id="rType" type="java.lang.String"--> + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <th:block layout:fragment="fields_before"></th:block> -<!--@thymesVar id="message" type="java.lang.String"--> + <div class="flex_column gap-1"> + <label class="form__label" for="enqueue-assignment">Assignment</label> + <select + id="enqueue-assignment" + name="assignment" + data-select + data-style="outlined" + data-placeholder="Select assignment" + required> + <option data-empty-option value=""></option> + <option + th:each="entry : ${assignments}" + th:value="${entry.key.id}" + th:text="|${entry.value} - ${entry.key.name}|"></option> + </select> + </div> -<body> -<section layout:fragment="subcontent"> - <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="flex_column gap-1"> + <label class="form__label" for="enqueue-type">Type</label> + <select + id="enqueue-type" + name="requestType" + data-select + data-style="outlined" + data-placeholder="Select type" + required> + <option data-empty-option value=""></option> + <option + th:each="type : ${T(nl.tudelft.queue.model.enums.RequestType).values()}" + th:value="${type}" + th:text="${type.displayName()}"></option> + </select> + </div> - <div class="page-sub-header"> - <h3 th:text="${'Session #' + qSession.id}"></h3> - </div> + <div class="flex_column gap-1"> + <label class="form__label" for="enqueue-room">Room</label> + <select + id="enqueue-room" + name="room" + data-select + data-style="outlined" + data-placeholder="Select room" + required> + <option data-empty-option value=""></option> + <option + th:each="room : ${rooms}" + th:value="${room.id}" + th:text="|${room.building.name} - ${room.name}|"></option> + </select> + </div> - <div class="alert alert-info mt-md-3" role="alert" th:unless="${#strings.isEmpty(message)}"> - <span th:text="${message}"></span> - </div> + <div class="flex_column gap-1"> + <label class="form__label" for="enqueue-question">Question</label> + <textarea + id="enqueue-question" + class="textfield" + name="question" + data-style="outlined" + placeholder="Write your question" + rows="3" + style="resize: none" + required + minlength="15" + maxlength="500"></textarea> + </div> - <form th:action="@{/lab/{id}/enqueue/{rType}(id=${qSession.id}, rType=${rType})}" th:object="${request}" method="post" - class="form-horizontal"> - <th:block layout:fragment="enqueue-body"> - </th:block> + <div class="flex_column gap-1"> + <label class="form__label" for="enqueue-comment">Comment</label> + <textarea + id="enqueue-comment" + class="textfield" + name="comment" + data-style="outlined" + placeholder="Help your TA find you" + rows="3" + style="resize: none" + maxlength="250"></textarea> + </div> + </div> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <button type="submit" name="enqueue" id="enqueue" - class="btn btn-success ctrl-enter-submit">Enqueue - </button> - <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + type="submit" + data-style="outlined"> + <span class="margin_right-1 fa-solid fa-hand"></span> + <span>Enqueue</span> + </button> + </div> </div> - </div> - </form> -</section> -</body> + </form> + </dialog> </html> diff --git a/src/main/resources/templates/lab/enqueue/exam.html b/src/main/resources/templates/lab/enqueue/exam.html new file mode 100644 index 0000000000000000000000000000000000000000..fc0a61962e29113a6b7cec1353a7db6a54b40756 --- /dev/null +++ b/src/main/resources/templates/lab/enqueue/exam.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{lab/enqueue}"> + <th:block layout:fragment="fields_before"> + <div class="flex_column gap-1"> + <label class="form__label" for="enqueue-time-slot">Time slot</label> + <select + id="enqueue-time-slot" + name="timeSlot.id" + data-select + data-style="outlined" + data-placeholder="Select time slot" + required> + <option + th:each="slot, iter : ${qSession.timeSlots.?[canTakeSlot()]}" + th:value="${slot.id}" + th:text="${slot.slot.toSentence()}" + th:selected="${iter.index == 0}"></option> + </select> + </div> + </th:block> +</html> diff --git a/src/main/resources/templates/lab/enqueue/regular.html b/src/main/resources/templates/lab/enqueue/regular.html new file mode 100644 index 0000000000000000000000000000000000000000..daf2701a5686a43b5ce0e46061402c9d9727ea24 --- /dev/null +++ b/src/main/resources/templates/lab/enqueue/regular.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{lab/enqueue}"></html> diff --git a/src/main/resources/templates/lab/remove.html b/src/main/resources/templates/lab/remove.html index 7223294af27cf7865ec7da8130505883a9310824..cfcddbc9eb499824eb7a666ca2f995445d85670b 100644 --- a/src/main/resources/templates/lab/remove.html +++ b/src/main/resources/templates/lab/remove.html @@ -1,51 +1,50 @@ -<!-- - - 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="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"--> -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> - -<!--@thymesVar id="qSession" type="nl.tudelft.queue.model.QueueSession"--> - -<body> -<section layout:fragment="subcontent"> - <div class="page-sub-header"> - <h3>Remove lab</h3> - </div> - - <form action="#" th:action="@{/lab/{id}/remove(id=${qSession.id})}" th:object="${qSession}" - class="form-horizontal" - method="post"> - <input type="hidden" th:field="*{id}"/> - - Are you sure you want to remove <strong th:text="${'session #' + qSession.id}"></strong>? - - <div class="text-center"> - <button class="btn btn-danger">Delete this session</button> - <small>or <a - th:href="@{/{ou}/{id}/labs(id=${ec == null ? edition.id : ec.id}, ou=${ec == null ? 'edition' : 'shared-edition'})}">go - back</a></small> - </div> - </form> -</section> -</body> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="remove-session" layout:fragment="dialog"> + <form th:action="@{/lab}" th:method="delete" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Delete session</h2> + </div> + <button class="hidden:from_small">Delete</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-6"> + <input name="id" type="hidden" /> + + <p class="font-400"> + Are you sure you want to delete + <span id="remove-session-name"></span> + ? + </p> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + data-type="negative"> + <span class="margin_right-1 fa-solid fa-trash-alt"></span> + <span>Delete session</span> + </button> + </div> + </div> + </form> + </dialog> </html> diff --git a/src/main/resources/templates/lab/view.html b/src/main/resources/templates/lab/view.html index 84dcdb866379ddeeff9c89b0ea9a5c92af796944..9aeb490995f0b594ba8e5e8aec9a74349a369aa4 100644 --- a/src/main/resources/templates/lab/view.html +++ b/src/main/resources/templates/lab/view.html @@ -1,136 +1,376 @@ -<!-- +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <head> + <title th:text="|${qSession.session.name}|"></title> + </head> - Queue - A Queueing system that can be used to handle labs in higher education - Copyright (C) 2016-2020 Delft University of Technology + <div layout:fragment="breadcrumbs" class="breadcrumbs"> + <a th:href="@{/}">My courses</a> + <span>></span> + <a + th:if="${ec != null}" + th:text="${ec.name}" + th:href="@{/shared-edition/{id}(id=${ec.id})}"></a> + <a + th:if="${edition != null}" + th:text="|${edition.course.name} - ${edition.name}|" + th:href="@{/edition/{id}(id=${edition.id})}"></a> + <span>></span> + <a th:text="${qSession.session.name}" th:href="@{/lab/{id}(id=${qSession.id})}"></a> + </div> - 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. + <th:block layout:fragment="content"> + <div + class="flex space_between align_items_center:from_small | flex_column:small margin_bottom-5:small"> + <h1 class="title margin_bottom-5:from_small" th:text="${qSession.session.name}"></h1> + <div class="flex gap-3 | flex_column:small"> + <button + th:if="${@permissionService.canEnqueueSelf(qSession.id)}" + id="enqueue-button" + class="button padding_inline-5 padding_block-3" + data-style="floating" + data-dialog="enqueue" + th:data-lab="${qSession.id}" + th:disabled="${not @permissionService.canEnqueueSelfNow(qSession.id)}"> + <span class="margin_right-1 fa-solid fa-hand"></span> + <span>Enqueue</span> + </button> + <th:block th:if="${@permissionService.canManageSession(qSession.id)}"> + <form + class="flex_column" + th:action="@{/lab/{qSession}/close-enqueue/{closeEnqueue}(qSession=${qSession.id}, closeEnqueue=${!qSession.enqueueClosed})}" + method="post"> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + th:data-type="${qSession.enqueueClosed} ? 'positive' : 'negative'"> + <span + class="margin_right-1 fa-solid" + th:classappend="${qSession.enqueueClosed} ? 'fa-lock-open' : 'fa-lock'"></span> + <span + th:text="${qSession.enqueueClosed} ? 'Open enqueuing' : 'Close enqueuing'"></span> + </button> + </form> + <a + class="button padding_inline-5 padding_block-3" + data-style="floating" + th:href="@{/lab/{id}/export(id=${qSession.id})}"> + <span class="margin_right-1 fa-solid fa-file-export"></span> + <span>Export session</span> + </a> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + data-dialog="create-session" + th:data-session="${qSession.id}" + data-wizard-type="edit"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit session</span> + </button> + </th:block> + </div> + </div> - 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. + <div class="flex_column"> + <th:block th:if="${@permissionService.canManageSession(qSession.id)}"> + <th:block th:if="${qSession.isActive()}"> + <div th:if="${qSession.isEnqueueingOpen()}" class="banner" data-type="check"> + <span class="fa-solid fa-check-circle"></span> + <span>This session is active and students can enqueue.</span> + </div> + <div th:if="${qSession.enqueueClosed}" class="banner" data-type="warning"> + <span class="fa-solid fa-warning"></span> + <span>This session is active but enqueuing is closed.</span> + </div> + </th:block> + <th:block th:if="${qSession.isBeforeSession()}"> + <div th:unless="${qSession.enqueueClosed}" class="banner" data-type="info"> + <span class="fa-solid fa-info-circle"></span> + <span + th:text="|This session will become active on ${#temporals.format(qSession.session.start, 'dd MMMM yyyy HH:mm')}.|"></span> + </div> + <div th:if="${qSession.enqueueClosed}" class="banner" data-type="warning"> + <span class="fa-solid fa-warning"></span> + <span + th:text="|This session will become active on ${#temporals.format(qSession.session.start, 'dd MMMM yyyy HH:mm')}, but students cannot enqueue as enqueuing is closed.|"></span> + </div> + </th:block> + <div th:if="${qSession.isAfterSession()}" class="banner" data-type="info"> + <span class="fa-solid fa-info-circle"></span> + <span>This session is over.</span> + </div> + </th:block> - 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/>. + <div class="flex | flex_column:small"> + <div class="rounded shadow info_card flex_grow"> + <h2 class="heading">Session info</h2> ---> -<!DOCTYPE html> -<html lang="en" xmlns:th="http://www.thymeleaf.org" - xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" - layout:decorate="~{edition/view}"> + <div class="flex_column gap-4 padding-5"> + <div class="flex_column gap-0"> + <span class="label">Slot</span> + <span + th:text="|${#temporals.format(qSession.session.start, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(qSession.session.end, 'dd MMMM yyyy HH:mm')}|"></span> + </div> + <div class="flex_column gap-0"> + <span class="label">Rooms</span> + <ul class="margin_left-6"> + <li + th:each="room : ${qSession.session.rooms}" + th:text="|${room.building.name} - ${room.name}|"></li> + </ul> + </div> + <div class="flex_column gap-0"> + <span class="label">Allowed requests</span> + <ul class="margin_left-6"> + <li + th:each="entry : ${assignments}" + th:text="|${entry.key.module.name} - ${entry.key.name} (${#strings.setJoin(types.get(entry.key.id), '/')})|"></li> + </ul> + </div> + </div> + </div> -<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.api.dto.Person"--> + <div class="rounded shadow info_card flex_grow"> + <h2 class="heading">Session status</h2> -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + <div class="flex_column gap-4 padding-5"> + <div class="flex_column gap-0"> + <span class="label">Active status</span> + <span + th:if="${qSession.isBeforeSession()}" + th:text="|This session will start on ${#temporals.format(qSession.session.start, 'dd MMMM yyyy HH:mm')}|"></span> + <span th:if="${qSession.isActive()}"> + This is an + <b>active</b> + session + </span> + <span + th:if="${qSession.isAfterSession()}" + th:text="|This session ended on ${#temporals.format(qSession.session.end, 'dd MMMM yyyy HH:mm')}|"></span> + </div> + <div class="flex_column gap-0"> + <span class="label">Enqueuing status</span> + <span th:if="${qSession.isEnqueueingOpen()}"> + Enqueuing is + <b>open</b> + </span> + <span th:if="${qSession.enqueueClosed}"> + Enqueuing is + <b>closed</b> + </span> + <span + th:if="${!qSession.isEnqueueingOpen() and !qSession.enqueueClosed}" + th:text="${qSession.closedReason()}"></span> + </div> + <div class="flex_column gap-0"> + <span class="label">Current size of queue</span> + <span th:if="${qSession.data.queue.size() == 1}">1 students</span> + <span + th:unless="${qSession.data.queue.size() == 1}" + th:text="|${qSession.data.queue.size()} students|"></span> + </div> + <div class="flex_column gap-0"> + <span class="label">Waiting time</span> + <div + th:with="avg = ${qSession.data.averageWaitingTime()}, cur = ${qSession.data.currentWaitingTime()}"> + <th:block th:if="${avg.isPresent() || cur.isPresent()}"> + <span + th:if="${avg.isPresent()}" + th:text="|${avg.getAsDouble()} minutes |"></span> + <th:block th:unless="${avg.isPresent()}"> + <span>Unknown</span> + </th:block> + <span>(average) /</span> + <span + th:if="${cur.isPresent()}" + th:text="|${cur.getAsDouble()} minutes |"></span> + <th:block th:unless="${cur.isPresent()}"> + <span>Unknown</span> + </th:block> + <span>(current)</span> + </th:block> + <th:block th:unless="${avg.isPresent() || cur.isPresent()}"> + <span>Unknown</span> + </th:block> + </div> + </div> + </div> + </div> -<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.QueueSessionViewDTO"--> + <th:block layout:fragment="additional_info"></th:block> + </div> -<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"--> -<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"--> -<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"--> + <th:block layout:fragment="additional_content"></th:block> -<!--@thymesVar id="requests" type="java.util.List<nl.tudelft.queue.dto.view.RequestViewDTO>"--> + <div + class="flex space_between align_items_center:from_small | flex_column:small align_items_stretch:small" + layout:fragment="request_header"> + <h2 class="subtitle margin_top-3">Requests</h2> -<!--@thymesVar id="current" type="nl.tudelft.queue.dto.view.RequestViewDTO"--> -<!--@thymesVar id="currentDto" type="nl.tudelft.queue.dto.patch.RequestPatchDTO"--> + <div class="flex_column align_items_start:from_small"> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + data-dialog="filters"> + <span class="margin_right-1 fa-solid fa-filter"></span> + <span>Filters</span> + <span th:text="|(${filter.countActiveFilters()})|"></span> + </button> + </div> + </div> -<!--@thymesVar id="types" type="java.util.Map<java.lang.Long, java.util.Set<java.lang.String>>"--> + <div th:if="${current != null}" class="rounded shadow info_card"> + <h3 class="heading">Current request</h3> -<head> - <title th:text="|Lab ${qSession.id}|"></title> + <div + class="padding-5 flex_column" + th:with="position = ${qSession.data.position(#authenticatedP.id)}"> + <div class="flex_column gap-0"> + <span class="label">Current position in queue</span> + <span + id="position" + th:text="${position == 0} ? 'Assistant is coming' : ${position}"></span> + </div> - <script th:inline="javascript" type="text/javascript"> - //<![CDATA[ - const labId = /*[[${qSession.id}]]*/ -1; - //]]> - </script> - <script type="text/javascript" src="/js/lab_view.js"></script> + <input + id="current-request" + name="request" + type="hidden" + th:value="${current.id}" /> - <th:block th:if="${@permissionService.canManageSession(qSession.data)}"> - <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"/> - </th:block> -</head> - -<body> -<th:block layout:fragment="subcontent"> - <div class="page-sub-header"> - <div class="float-right"> - <th:block th:if="${current == null && @permissionService.canEnqueueSelf(qSession.id)}"> - <a href="#" th:href="@{/lab/{id}/enqueue(id=${qSession.id})}" - class="btn btn-success" style="margin: 0.25em" - th:classappend="${!@permissionService.canEnqueueSelfNow(qSession.id)} ? 'disabled'" - th:disabled="${!@permissionService.canEnqueueSelfNow(qSession.id)}">Enqueue</a> - </th:block> + <div class="flex_column gap-1 align_items_start"> + <label class="label" for="current-room">Room</label> + <select id="current-room" name="room" data-select data-style="outlined"> + <option + th:each="room : ${rooms}" + th:value="${room.id}" + th:text="|${room.building.name} - ${room.name}|" + th:selected="${room.id == current.room.id}"></option> + </select> + </div> - <div class="row" - th:if="${@permissionService.canManageSession(qSession.data)}"> - <a class="btn btn-primary" style="margin: 0.25em" - th:href="@{/lab/{id}/export(id=${qSession.id})}" download>Export lab</a> - <form class="form-inline" - th:action="@{/lab/{id}/close-enqueue/{bool}(id=${qSession.id}, bool=${!qSession.enqueueClosed})}" - method="post"> - <button th:unless="${qSession.enqueueClosed}" class="btn btn-danger" style="margin: 0.25em" - type="submit">Close enqueueing - </button> - <button th:if="${qSession.enqueueClosed}" class="btn btn-success" style="margin: 0.25em" - type="submit">Open enqueueing - </button> - </form> + <div class="flex_column gap-1"> + <label class="label" for="current-comment">Comment</label> + <textarea + autocomplete="off" + class="textfield" + data-show-valid + style="resize: none; max-width: 25rem" + id="current-comment" + name="comment" + th:text="${current.comment}" + rows="3" + maxlength="250" + data-style="outlined"></textarea> + </div> - <a th:href="@{/lab/{id}/edit(id=${qSession.id})}" style="margin: 0.25em" class="btn btn-secondary"> - <i class="fa fa-pencil" aria-hidden="true"></i> - </a> - </div> - </div> + <div class="flex_column gap-1"> + <label class="label" for="current-question">Question</label> + <textarea + autocomplete="off" + class="textfield" + data-show-valid + style="resize: none; max-width: 25rem" + id="current-question" + name="question" + th:text="${current.question}" + rows="3" + minlength="15" + maxlength="500" + data-style="outlined"></textarea> + </div> - <h3 th:text="'Lab #' + ${qSession.id} + ': ' + ${qSession.session.name}"></h3> - </div> - - <div class="row"> - <th:block th:replace="lab/view/components/selection-result :: selection-result"> - </th:block> - </div> - - <div th:if="${current != null}" - class="row"> - <th:block th:replace="lab/view/components/current-request :: current-request"> - </th:block> + <form + th:if="${@permissionService.canRevokeFromSession(qSession.id)}" + th:action="@{/lab/{id}/revoke(id=${qSession.id})}" + method="post"> + <button + id="unenqueue-button" + class="button padding_inline-5 padding_block-3" + data-type="negative" + data-style="outlined"> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Unenqueue</span> + </button> + </form> + </div> + </div> - <th:block layout:fragment="current-request-edit"> - <th:block th:replace="lab/view/components/current-request-edit :: current-request-edit"> + <th:block layout:fragment="request_list"> + <th:block + layout:replace="~{request/table :: table(showAssistants=true)}"></th:block> </th:block> - </th:block> - </div> + </div> + </th:block> - <div class="card-deck mt-3"> - <th:block layout:fragment="session-info"> + <th:block layout:fragment="overlays"> + <th:block + th:with="returnPath = |lab/${qSession.id}|, assignments = ${assignments.keySet()}"> + <th:block layout:replace="~{request/filters :: dialog}"></th:block> </th:block> - </div> - - <div class="mt-3"> - <th:block layout:fragment="request-table"> + <th:block + layout:replace="~{lab/enqueue/__${qSession.type.name().toLowerCase()}__}"></th:block> + <th:block th:with="assignments = ${allAssignments}"> + <th:block layout:replace="~{lab/create :: dialog}"></th:block> </th:block> - </div> + <th:block layout:replace="~{request/actions/approve :: dialog}"></th:block> + <th:block layout:replace="~{request/actions/reject :: dialog}"></th:block> + <th:block layout:replace="~{request/actions/forward :: dialog}"></th:block> + <th:block layout:fragment="additional_overlays"></th:block> + </th:block> - <script src="/js/map_loader.js"></script> - <script type="text/javascript"> - $(function () { - const roomId = $('select').val(); - if (roomId) { - updateRequestInfo(roomId); + <th:block layout:fragment="script"> + <script th:inline="javascript"> + /*<![CDATA[*/ + const labId = /*[[${qSession.id}]]*/ 0; + /*]]>*/ + </script> + <script src="/js/lab_view.js"></script> + <th:block layout:replace="~{lab/create :: create_script}"></th:block> + <script> + function updateRequestInfo() { + const room = $("#current-room").val(); + const question = $("#current-question").val(); + const comment = $("#current-comment").val(); + if (question.length < 15 || question.length > 500 || comment.length > 250) return; + + $.ajax({ + url: `/request/${$("#current-request").val()}/update-request-info`, + type: "post", + data: { + room: room, + question: question, + comment: comment, + }, + success: () => {}, + error: () => alert("Error updating request"), + }); } - }); - </script> -</th:block> -<th:block layout:fragment="outside-content"> - <th:block layout:fragment="exam-request-table"> + $(() => { + $("#current-room").change(updateRequestInfo); + $("#current-question").on("stopTyping", updateRequestInfo); + $("#current-comment").on("stopTyping", updateRequestInfo); + + $("[data-session]").click(async function () { + wizardType = $(this).data("wizard-type"); + $("[data-create-session-wizard]").each(function () { + if (!$(this).data("create-session-wizard").includes(wizardType)) + $(this).addClass("hidden"); + }); + const sessionId = $(this).data("session"); + $("#create-session-id").val(sessionId); + const session = await fetch(`/lab/json/${sessionId}`).then(res => res.json()); + sessionType = session.type; + getForms().forEach(f => getForm(f).trigger("edit", [session])); + getForm("type").trigger("edit", [session]); + }); + }); + </script> + <th:block layout:fragment="additional_scripts"></th:block> </th:block> -</th:block> -</body> </html> diff --git a/src/main/resources/templates/lab/view/components/edit_slots.html b/src/main/resources/templates/lab/view/components/edit_slots.html new file mode 100644 index 0000000000000000000000000000000000000000..f2b6346416ab4fe9a445c28bc5d52565f446e765 --- /dev/null +++ b/src/main/resources/templates/lab/view/components/edit_slots.html @@ -0,0 +1,130 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="edit-time-slots" layout:fragment="dialog"> + <form + th:action="@{/lab/{labId}/capacities(labId=${qSession.id})}" + th:method="patch" + class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Edit time slots</h2> + </div> + <button class="hidden:from_small">Edit</button> + </div> + + <div class="dialog__body"> + <div + class="flex gap-7 margin_bottom-7 | flex_column:medium" + style="min-width: min(50vw, 40rem)"> + <div class="flex_column gap-5"> + <div class="flex_column gap-1"> + <label class="form__label" for="bulk-time-slots-start"> + Time slots from + </label> + <select id="bulk-time-slots-start" data-select data-style="outlined"> + <option + th:each="slot, iter : ${qSession.timeSlots}" + th:unless="${iter.count == qSession.timeSlots.size()}" + th:selected="${slot == qSession.timeSlots.^[#this.slot.opensAt > #temporals.createNow()]}" + th:value="${slot.slot.opensAt}" + th:text="${#temporals.format(slot.slot.opensAt, 'HH:mm')}"></option> + </select> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="bulk-time-slots-end"> + Time slots to + </label> + <select id="bulk-time-slots-end" data-select data-style="outlined"> + <option + th:each="slot, iter : ${qSession.timeSlots}" + th:unless="${iter.index == 0}" + th:selected="${iter.count == qSession.timeSlots.size()}" + th:value="${slot.slot.closesAt}" + th:text="${#temporals.format(slot.slot.closesAt, 'HH:mm')}"></option> + </select> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="bulk-time-slots-capacity"> + New capacity + </label> + <input + id="bulk-time-slots-capacity" + class="textfield" + type="number" + value="0" + data-style="outlined" /> + </div> + + <button + id="bulk-update-capacity" + type="button" + class="button padding_inline-5 padding_block-3" + data-style="outlined"> + Update capacities + </button> + </div> + + <div class="grid gap-5 auto_fill flex_grow" style="--min-size: 5rem"> + <div class="flex_column gap-1" th:each="slot : ${qSession.timeSlots}"> + <label + class="form__label" + th:for="|edit-slot-${slot.id}|" + th:text="|${#temporals.format(slot.slot.opensAt, 'HH:mm')} - ${#temporals.format(slot.slot.closesAt, 'HH:mm')}|"></label> + <input + th:id="|edit-slot-${slot.id}|" + th:name="|capacities[${slot.id}]|" + type="number" + class="textfield" + data-style="outlined" + th:min="${slot.requests.size()}" + th:value="${slot.capacity}" + data-slot + th:data-slot-start="${slot.slot.opensAt}" /> + </div> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit time slots</span> + </button> + </div> + </div> + </form> + + <script> + $(() => { + $("#bulk-update-capacity").click(function () { + const start = moment($("#bulk-time-slots-start").val()); + const end = moment($("#bulk-time-slots-end").val()); + const capacity = $("#bulk-time-slots-capacity").val(); + + $("[data-slot]").each((i, s) => { + const slotStart = moment($(s).data("slot-start")); + if (slotStart.isSameOrAfter(start) && slotStart.isBefore(end)) { + $(s).val(capacity); + } + }); + }); + }); + </script> + </dialog> +</html> diff --git a/src/main/resources/templates/lab/view/components/exam_info.html b/src/main/resources/templates/lab/view/components/exam_info.html new file mode 100644 index 0000000000000000000000000000000000000000..52a7291707d37a357e483e03e1ff4504d40f1d14 --- /dev/null +++ b/src/main/resources/templates/lab/view/components/exam_info.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div layout:fragment="card" class="rounded shadow info_card flex_grow flex_column gap-0"> + <h2 class="heading">Exam info</h2> + + <progress class="progress" max="100" th:value="${qSession.percentageHandled}"></progress> + + <div class="flex_column gap-4 padding-5"> + <div class="flex_column gap-0"> + <span class="label">Slot selection opens at</span> + <span + th:text="${#temporals.format(qSession.slottedLabConfig.selectionOpensAt, 'dd MMMM yyyy HH:mm')}"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Capacity</span> + <span th:text="|${qSession.slottedLabConfig.capacity} requets per slot|"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Time slot duration</span> + <span th:text="|${qSession.slottedLabConfig.duration} minutes per slot|"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Minimum exam session percentage</span> + <span th:text="|${qSession.examLabConfig.percentage}%|"></span> + </div> + </div> + </div> +</html> diff --git a/src/main/resources/templates/lab/view/components/slot_info.html b/src/main/resources/templates/lab/view/components/slot_info.html new file mode 100644 index 0000000000000000000000000000000000000000..dd5dd4ddad0610d90638a642dc2c1a263d08bbb6 --- /dev/null +++ b/src/main/resources/templates/lab/view/components/slot_info.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div layout:fragment="card" class="rounded shadow info_card flex_grow"> + <h2 class="heading">Time slot info</h2> + + <div class="flex_column gap-4 padding-5"> + <div class="flex_column gap-0"> + <span class="label">Slot selection opens at</span> + <span + th:text="${#temporals.format(qSession.slottedLabConfig.selectionOpensAt, 'dd MMMM yyyy HH:mm')}"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Time slot duration</span> + <span th:text="|${qSession.slottedLabConfig.duration} minutes per slot|"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Enqueuing rules</span> + <span th:if="${qSession.canSelectDueSlots}"> + Students can take slots that have passed + </span> + <span th:unless="${qSession.canSelectDueSlots}"> + Students cannot take slots that have passed + </span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Request taking rules</span> + <span th:if="${qSession.earlyOpenTime == 0}"> + A slot request can be taken from the start of that slot + </span> + <span + th:unless="${qSession.earlyOpenTime == 0}" + th:text="|A slot request can be taken from ${qSession.earlyOpenTime} minutes before the start of that slot|"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Number of booked slots</span> + <span th:text="|${qSession.slotsOccupied} / ${qSession.maxSlots}|"></span> + </div> + </div> + </div> +</html> diff --git a/src/main/resources/templates/lab/view/exam.html b/src/main/resources/templates/lab/view/exam.html index 79eba6e8ac877e7ab63ae95e6c943af03f4bcf8c..c75eefac811e9d069ee55f4cb8858581de3cae63 100644 --- a/src/main/resources/templates/lab/view/exam.html +++ b/src/main/resources/templates/lab/view/exam.html @@ -1,48 +1,142 @@ -<!-- - - 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="~{lab/view/lab}"> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{lab/view}"> + <head> + <style> + .progress { + background-color: var(--neutral-800); + border: 1px solid var(--neutral-700); + color: var(--green-500); + max-height: 1rem; + min-width: 100%; + } + .progress::-moz-progress-bar, + .progress::-webkit-progress-bar { + background-color: var(--green-500); + } + </style> + </head> -<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.api.dto.Person"--> + <th:block layout:fragment="additional_info"> + <th:block layout:replace="~{lab/view/components/exam_info :: card}"></th:block> + </th:block> -<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.labs.ExamLabViewDTO"--> -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> + <th:block + th:if="${@permissionService.canViewSessionRequests(qSession.id) or current != null}" + layout:fragment="request_header"> + <h2 class="subtitle">Requests</h2> + </th:block> + <div + th:if="${@permissionService.canViewSessionRequests(qSession.id)}" + class="grid gap-5 padding_inline-1 align_items_start" + th:style="|--columns: ${qSession.examTimeSlots.size()}; overflow-x: scroll;|" + layout:fragment="request_list"> + <div + class="rounded shadow flex_column gap-0 margin_bottom-5 timeslot" + th:each="slot : ${qSession.examTimeSlots}"> + <div class="heading flex_column gap-3"> + <div class="flex space_between align_items_center"> + <div class="flex gap-3 align_items_center"> + <h3 th:text="${slot.slot.toSentence()}" style="white-space: nowrap"></h3> + <span + th:if="${slot.active}" + class="chip" + style="font-weight: initial" + data-type="positive"> + Active + </span> + <span + th:unless="${slot.active}" + class="chip" + style="font-weight: initial" + data-type="negative"> + Inactive + </span> + </div> + <div class="flex gap-3"> + <form + th:action="@{/time-slot/{slotId}/close(slotId=${slot.id})}" + method="post"> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + data-type="negative" + th:disabled="${not slot.active or slot.slot.closed()}"> + <span class="fa-solid fa-lock"></span> + <span>Close</span> + </button> + </form> + <form + th:action="@{/time-slot/{slotId}/take-next(slotId=${slot.id})}" + data-take-next + method="get" + th:with="open = ${@labRequestRepository.countNextExamRequests(slot.id, #authenticatedP)}"> + <button + class="button padding_inline-4 padding_block-1" + data-style="outlined" + th:disabled="${open == 0 or not slot.active}" + th:if="${qSession.isActive()}"> + <span class="fa-solid fa-hand"></span> + <span th:text="|Take next (${open})|"></span> + </button> + </form> + </div> + </div> -<!--@thymesVar id="requests" type="java.util.List<nl.tudelft.queue.model.TimeSlot>"--> + <div style="font-weight: initial"> + <span th:text="|${slot.handled} handled|"></span> + <span>/</span> + <span th:text="|${slot.occupied} occupied|"></span> + <span>/</span> + <span th:text="|${slot.capacity}|"></span> + </div> + </div> -<!--@thymesVar id="current" type="nl.tudelft.queue.dto.view.RequestViewDTO"--> + <progress + class="progress" + max="1" + th:value="${slot.handled / (1.0 * slot.capacity)}"></progress> -<head> - <title th:text="'Exam Lab #' + ${qSession.id}"></title> -</head> + <ul class="list" role="list"> + <li th:if="${slot.examRequests.isEmpty()}">No requests yet</li> + <li + th:each="request : ${slot.examRequests}" + class="flex align_items_center gap-3" + data-selectable + th:data-request="${request.id}"> + <button class="visually_hidden" th:data-request="${request.id}"></button> + <span + class="chip" + th:data-type="${request.eventInfo.status.colourClass}" + data-status + th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span> + <span th:text="${request.requesterName}"></span> + </li> + </ul> + </div> + </div> -<body> -<th:block layout:fragment="additional-lab-info"> - <th:block th:replace="lab/view/components/exam-lab-info :: exam-lab-info"> - </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> -</th:block> -</body> + <script layout:fragment="additional_scripts"> + $(() => { + $("[data-request]").click(function () { + fetchAndOpenDialog(`/request/${$(this).data("request")}`); + }); + $("[data-take-next]").submit(function (event) { + event.preventDefault(); + $.get($(this).attr("action"), html => { + const element = $(html).filter((i, e) => $(e).is("dialog")); + if (element.is("dialog")) { + $(".page").append(element); + openDialog(element.attr("id")); + configureDialog(element); + } else { + window.location.reload(); + } + }); + }); + }); + </script> </html> diff --git a/src/main/resources/templates/lab/view/limited_capacity.html b/src/main/resources/templates/lab/view/limited_capacity.html new file mode 100644 index 0000000000000000000000000000000000000000..3cc5aec198d91e0b0aaf338f4818e2be7bf75eb7 --- /dev/null +++ b/src/main/resources/templates/lab/view/limited_capacity.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{lab/view}"></html> diff --git a/src/main/resources/templates/lab/view/regular.html b/src/main/resources/templates/lab/view/regular.html index 4121caae9cfcacbbc5b051057bf84200ee4f4418..3cc5aec198d91e0b0aaf338f4818e2be7bf75eb7 100644 --- a/src/main/resources/templates/lab/view/regular.html +++ b/src/main/resources/templates/lab/view/regular.html @@ -1,35 +1,6 @@ -<!-- - - 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/>. - ---> -<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" - layout:decorate="~{lab/view/lab}"> - -<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.LabViewDTO"--> - -<head> - <title th:text="'Lab #' + ${qSession.id}"></title> -</head> - -<body> -<th:block layout:fragment="request-table"> - <th:block th:replace="lab/view/components/full-request-table :: full-request-table"> - </th:block> -</th:block> -</body> -</html> +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{lab/view}"></html> diff --git a/src/main/resources/templates/lab/view/slotted.html b/src/main/resources/templates/lab/view/slotted.html index 85479108743166b35bbbecaab1d866d9cf9e0200..ccab39413c1b7c5d5c07376d63c84a8cf27e51c2 100644 --- a/src/main/resources/templates/lab/view/slotted.html +++ b/src/main/resources/templates/lab/view/slotted.html @@ -1,45 +1,73 @@ -<!-- +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{lab/view}"> + <head> + <style> + .timeslot progress { + background-color: var(--neutral-700); + border: none; + color: var(--green-500); + max-height: 0.5rem; + min-width: 100%; + } + .timeslot progress[data-full="true"] { + color: var(--red-500); + } + .timeslot progress::-moz-progress-bar, + .timeslot progress::-webkit-progress-bar { + background-color: var(--green-500); + } + .timeslot progress[data-full="true"]::-moz-progress-bar, + .timeslot progress[data-full="true"]::-webkit-progress-bar { + background-color: var(--red-500); + } + </style> + </head> - 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/>. - ---> -<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" - layout:decorate="~{lab/view/lab}"> - -<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.labs.SlottedLabViewDTO"--> - -<head> - <title th:text="'Lab #' + ${qSession.id}"></title> -</head> - -<body> -<th:block layout:fragment="additional-lab-info"> - <th:block th:replace="lab/view/components/slotted-lab-info :: slotted-lab-info"> + <th:block layout:fragment="additional_info"> + <th:block layout:replace="~{lab/view/components/slot_info :: card}"></th:block> </th:block> -</th:block> -<th:block layout:fragment="request-table"> - <th:block th:replace="lab/view/components/slots-info :: slots-info"> + <th:block layout:fragment="additional_content"> + <div + class="flex space_between align_items_center:from_small | flex_column:small align_items_stretch:small"> + <h2 class="subtitle margin_top-3">Time slots</h2> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + data-dialog="edit-time-slots"> + <span class="fa-solid fa-pencil-alt"></span> + <span>Edit time slots</span> + </button> + </div> + <div class="grid auto_fill gap-3" style="--min-size: 9rem"> + <div + class="rounded shadow info_card flex_column gap-0 timeslot" + style="white-space: nowrap" + th:each="timeSlot : ${qSession.timeSlots}"> + <div class="padding-3 text_align_center"> + <span + th:text="|${#temporals.format(timeSlot.slot.opensAt, 'HH:mm')} - ${#temporals.format(timeSlot.slot.closesAt, 'HH:mm')}|"></span> + <!-- prettier-ignore --> + <span>(</span> + <span th:text="${timeSlot.requests.size()}"></span> + <span>/</span> + <span th:text="${timeSlot.capacity}"></span> + <span>)</span> + </div> + <progress + aria-hidden="true" + th:value="${timeSlot.requests.size}" + th:max="${timeSlot.capacity}" + th:data-full="${timeSlot.requests.size == timeSlot.capacity}"></progress> + </div> + </div> </th:block> - <div class="mt-3"></div> - - <th:block th:replace="lab/view/components/full-request-table :: full-request-table"> + <th:block layout:fragment="additional_overlays"> + <th:block layout:replace="~{lab/view/components/edit_slots :: dialog}"></th:block> </th:block> -</th:block> -</body> </html> diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index 7722dc4fd877670ad6e2cd3b31f33a460ab18a19..861fddd6bd670370d89d58a75dd5923567e60903 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -18,224 +18,77 @@ --> <!DOCTYPE html> -<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> -<head> - <meta charset="utf-8"/> - <meta http-equiv="X-UA-Compatible" content="IE=edge"/> - <meta http-equiv="Content-Language" content="en_GB"/> - <meta name="viewport" content="width=device-width, initial-scale=1"/> - - <meta name="description" content="Queue system"/> - <meta name="author" content=""/> - <meta name="_csrf_header" th:content="${_csrf.headerName}"/> - <meta name="_csrf" th:content="${_csrf.token}"/> - - <link rel="icon" href="/favicon.ico"/> - - <title th:unless="${@thymeleafConfig.isTheDay()}">Queue</title> - <title th:if="${@thymeleafConfig.isTheDay()}">Stack</title> - <title>Queue</title> - - <link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/> - <link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/> - <link rel="stylesheet" href="/webjars/font-awesome/css/v4-shims.css"/> - <link rel="stylesheet" type="text/css" href="/css/global.css"/> - - <script src="/webjars/jquery/jquery.min.js"></script> - <script src="/webjars/jquery-cookie/jquery.cookie.js"></script> - <script src="/webjars/bootstrap/js/bootstrap.bundle.min.js"></script> - - <script src="/webjars/sockjs-client/sockjs.min.js"></script> - <script src="/webjars/stomp-websocket/stomp.min.js"></script> - - <script src="/js/global.js"></script> - - <script type="application/javascript"> - $.ajaxSetup({ - headers: { - "X-CSRF-TOKEN": $("meta[name='_csrf']").attr("content") - } - }); - </script> - - <script th:if="${#authenticatedP != null}" th:inline="javascript" type="application/javascript"> - //<![CDATA[ - const authenticatedId = /*[[${#authenticatedP.id}]]*/ -1; - //]]> - </script> -</head> - -<!--@thymesVar id="page" type="java.lang.String"--> - -<!--@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"> - <!-- 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> - <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-nav" - aria-controls="navbar-nav" aria-expanded="false" aria-label="Toggle navigation"> - <i class="fa fa-bars" style="color:white;" aria-hidden="true"></i> - </button> - <div class="collapse navbar-collapse" id="navbar-nav"> - <ul class="navbar-nav nav-pills mr-auto mt-2 mt-lg-0"> - <th:block th:if="${#authenticatedP != null}"> - <li class="nav-item"> - <a class="nav-link" - th:classappend="${page == 'my-courses' ? 'active' : ''}" - th:href="@{/}">My Courses</a></li> - <li class="nav-item"> - <a class="nav-link" - th:classappend="${page == 'catalog' ? 'active' : ''}" - th:href="@{/editions}">Catalog</a></li> - <li class="nav-item" th:if="${@permissionService.canViewRequests()}"> - <a class="nav-link" - th:href="@{/requests}" - th:classappend="${page == 'requests' ? 'active' : ''}">Requests</a></li> - <li class="nav-item" th:if="${@permissionService.isAdmin()}"> - <a class="nav-link" - th:href="@{/admin}" - th:classappend="${page == 'admin' ? 'active' : ''}">Admin</a> - </li> - </th:block> - </ul> - <!-- Specifically for logged in users to get to personal pages --> - <ul class="navbar-nav"> - <th:block th:if="${#authenticatedP != null}"> - <li class="nav-item dropdown"> - <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" - role="button" aria-haspopup="true" aria-expanded="false" - th:text="${#authenticatedP.displayName}"> - <span class="caret"></span> - </a> - <div class="dropdown-menu dropdown-menu-right"> - <a class="dropdown-item text-dark" th:href="@{/history}">Request history</a> - <a class="dropdown-item text-dark" - th:href="@{/feedback/{id}(id=${#authenticatedP.id})}" - th:if="${@permissionService.canViewOwnFeedback()}">Feedback</a> - <form th:action="@{/logout}" method="post"> - <button class="dropdown-item" type="submit">Logout</button> - </form> - </div> - </li> - </th:block> - <li class="nav-item" th:unless="${#authenticatedP != null}"> - <a class="nav-link" th:href="@{/login}">Login</a> - </li> - </ul> - </div> -</nav> - -<main class="flex-fill fluid-container"> - <!-- Announcement banners --> - <div class="fluid-container justify-content-center mt-3 mb-3 ml-3 mr-3"> - <th:block th:each="announcement : ${@announcementRepository.findActiveAnnouncements()}"> - <div class="alert fade d-none" role="alert" th:data-announcement-id="|alert-${announcement.id}|" - th:styleappend="|color: ${'#' + announcement.hexTextColour()}; background-color: ${'#' + announcement.hexBackgroundColour()};|" - th:classappend="${announcement.isDismissible ? 'alert-dismissible' : ''}"> - <strong th:text="${announcement.message}"></strong> - <button type="button" class="close" data-dismiss="alert" aria-label="Close" - th:if="${announcement.isDismissible}"> - <span aria-hidden="true">×</span> - </button> - </div> - </th:block> - </div> - - <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"> - <th:block layout:fragment="content" class="content"> - <p>Page content goes here</p> - </th:block> - </div> - </div> - <th:block layout:fragment="outside-content"> - </th:block> - <div class="modal fade" id="sessionExpiredModal" tabindex="-1" role="dialog"> - <div class="modal-dialog modal-dialog-centered" role="document"> - <div class="modal-content"> - <div class="modal-body"> - <h3>Your session has expired</h3> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-primary" data-dismiss="modal">Close - </button> - <a th:href="@{/login}" class="btn btn-primary">Login</a> - </div> - </div> - </div> - </div> -</main> - -<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="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> - <div class="col-4 col-sm-2"> - <a class="text-muted btn" th:href="@{/about}">About</a> - </div> - <div class="col-3 col-sm-2"> - <img th:src="@{${@instituteProperties.logo}}" - class="float-right pr-2 mr-0" - height="40px" - alt="Logo"/> - </div> - </div> - </div> - </div> - </div> - - <th:block th:if="${#authenticatedP != null}"> - <!-- Import push.js --> - <script src="/js/push.js"></script> - <script th:inline="javascript"> - /*<![CDATA[*/ - - let timeout = /*[[${#httpSession.getMaxInactiveInterval()}]]*/ '10'; - setTimeout(function() { - $('#sessionExpiredModal').modal('show') - }, parseInt(timeout) * 1000) - - /*]]>*/ - </script> - </th:block> - <script type="application/javascript"> - // Initialize the dismissed announcements cookie when it does not yet exist - if (typeof $.cookie("dismissed-announcements") === "undefined") { - $.cookie("dismissed-announcements", "[]", { path: "/" }) - } - - // For every alert, check whether it is dismissed. If an alert is not dismissed, show it - $(() => { - const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements")); - - $(".alert").each(function() { - const id = $(this).data("announcement-id"); - if (!dismissedAnnouncements.includes(id)) { - $(this).removeClass("d-none").addClass("show"); - } +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <head> + <meta charset="utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <meta http-equiv="Content-Language" content="en_GB" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + + <meta name="description" content="Queue system" /> + <meta name="author" content="" /> + <meta name="_csrf_header" th:content="${_csrf.headerName}" /> + <meta name="_csrf" th:content="${_csrf.token}" /> + + <link rel="icon" href="/favicon.ico" /> + + <title th:unless="${@thymeleafConfig.isTheDay()}">Queue</title> + <title th:if="${@thymeleafConfig.isTheDay()}">Stack</title> + <title>Queue</title> + + <link rel="stylesheet" href="/webjars/font-awesome/css/all.css" /> + <link rel="stylesheet" href="/webjars/font-awesome/css/v4-shims.css" /> + <link rel="stylesheet" type="text/css" href="/css/global.css" /> + + <script src="/webjars/jquery/jquery.min.js"></script> + <script src="/webjars/jquery-cookie/jquery.cookie.js"></script> + + <script src="/webjars/sockjs-client/sockjs.min.js"></script> + <script src="/webjars/stomp-websocket/stomp.min.js"></script> + + <script src="/webjars/momentjs/2.24.0/min/moment.min.js"></script> + + <script src="/js/global.js"></script> + + <script type="application/javascript"> + $.ajaxSetup({ + headers: { + "X-CSRF-TOKEN": $("meta[name='_csrf']").attr("content"), + }, }); - }); - - // Register an event listener that adds a dismissed announcement to the dedicated cookie - $(".alert").on("closed.bs.alert", function() { - const id = $(this).data("announcement-id"); + </script> - const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements")); - dismissedAnnouncements.push(id); + <script + th:if="${#authenticatedP != null}" + th:inline="javascript" + type="application/javascript"> + //<![CDATA[ + const authenticatedId = /*[[${#authenticatedP.id}]]*/ -1; + //]]> + </script> + </head> + + <body> + <th:block layout:fragment="container"></th:block> + + <script src="/js/announcement.js"></script> + <script src="/js/select.js"></script> + <script src="/js/file.js"></script> + <th:block th:if="${#authenticatedP != null}"> + <script src="/js/push.js"></script> + <script th:inline="javascript"> + /*<![CDATA[*/ + let timeout = /*[[${#httpSession.getMaxInactiveInterval()}]]*/ "10"; + setTimeout(function () { + $("#sessionExpiredModal").modal("show"); + }, parseInt(timeout) * 1000); + /*]]>*/ + </script> + </th:block> - $.cookie("dismissed-announcements", JSON.stringify(dismissedAnnouncements), { path: "/" }); - }); - </script> -</footer> -</body> + <th:block layout:fragment="scripts"></th:block> + </body> </html> diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 128efa600bbae86661b3ba162c0c75992d039d63..c254adadce4dd0d3c26dacec875d6a9ce25ed9af 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -1,63 +1,112 @@ -<!-- - - 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="~{layout}"> -<head> -</head> - -<body> -<section layout:fragment="content"> - <div class="page-header"> - <h1>Login</h1> - </div> - - <div class="alert alert-danger" role="alert" th:if="${param.error}"> - <span>Invalid username and password.</span> - </div> - - <div class="alert alert-info" role="alert" th:if="${param.logout}"> - <span>You have been logged out.</span> - </div> - - <form action="#" th:action="@{/perform-login}" class="form-horizontal" method="post" id="login-form"> - <div class="form-group"> - <label for="input-username" class="col-sm-2 control-label">Username</label> - - <div class="col-sm-8"> - <input type="text" id="input-username" name="username" class="form-control" placeholder="Username"/> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <div layout:fragment="content" class="flex_column gap-7"> + <form + th:action="@{/perform-login}" + method="post" + class="flex_column" + style="max-width: 20rem"> + <div> + <div class="flex_column gap-0 margin_bottom-3"> + <label class="form__label" for="username">Username</label> + <input class="textfield" data-style="floating" id="username" name="username" /> + </div> + <div class="flex_column gap-0"> + <label class="form__label" for="password">Password</label> + <input + class="textfield" + data-style="floating" + id="password" + name="password" + type="password" /> + </div> </div> - </div> - <div class="form-group"> - <label for="input-password" class="col-sm-2 control-label">Password</label> - - <div class="col-sm-8"> - <input type="password" id="input-password" name="password" class="form-control" placeholder="Password"/> - </div> - </div> - <input type="hidden" name="password" value=""/> - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <input type="submit" class="btn btn-primary" value="Login"/> + <div> + <button class="button padding_inline-5 padding_block-3" data-style="floating"> + Log in + </button> </div> + </form> + + <div th:if="${@environment.activeProfiles[0] == 'development'}"> + <form class="grid auto_fill gap-5" th:action="@{/perform-login}" method="post"> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + name="username" + value="admin1"> + <span class="margin_right-1 fa-solid fa-screwdriver-wrench"></span> + <span>Admin 1</span> + </button> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + name="username" + value="cseteacher1"> + <span class="margin_right-1 fa-solid fa-user-graduate"></span> + <span>CSE Teacher 1</span> + </button> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + name="username" + value="cseteacher2"> + <span class="margin_right-1 fa-solid fa-user-graduate"></span> + <span>CSE Teacher 2</span> + </button> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + name="username" + value="csestudent1"> + <span class="margin_right-1 fa-brands fa-tiktok"></span> + <span>CSE Student 1</span> + </button> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + name="username" + value="csestudent2"> + <span class="margin_right-1 fa-brands fa-tiktok"></span> + <span>CSE Student 2</span> + </button> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + name="username" + value="csestudent3"> + <span class="margin_right-1 fa-brands fa-tiktok"></span> + <span>CSE Student 3</span> + </button> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + name="username" + value="csestudent4"> + <span class="margin_right-1 fa-brands fa-tiktok"></span> + <span>CSE Student 4</span> + </button> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + name="username" + value="csestudent5"> + <span class="margin_right-1 fa-brands fa-tiktok"></span> + <span>CSE Student 5</span> + </button> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + name="username" + value="csestudent6"> + <span class="margin_right-1 fa-brands fa-tiktok"></span> + <span>CSE Student 6</span> + </button> + </form> </div> - </form> -</section> -</body> + </div> </html> diff --git a/src/main/resources/templates/module/create.html b/src/main/resources/templates/module/create.html index 6185fae1159f959de09f38a634bea3f3f77b3505..fd2713da2149509cfc18f4b9c93449e8b5d2a370 100644 --- a/src/main/resources/templates/module/create.html +++ b/src/main/resources/templates/module/create.html @@ -1,64 +1,52 @@ -<!-- - - 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}"> -<head> - <title th:text="|Create Module for ${'#' + edition.id}|"></title> - - <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script> - <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/> -</head> - -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> - -<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueModuleCreateDTO"--> - -<body> -<section layout:fragment="subcontent"> - <div class="page-header"> - <h3>Create Module</h3> - </div> - - <form th:with="action = @{/edition/{id}/modules/create(id=${edition.id})}" - th:action="${action}" th:object="${dto}" - class="form-horizontal" method="post"> - <input type="hidden" name="id" th:value="${edition.id}"> - - <div class="form-group form-row"> - <label for="title-input" class="col-md-2 col-form-label">Name:</label> - <div class="col-md-4"> - <input id="title-input" - type="text" class="form-control" - required="required" th:field="*{name}" - placeholder="Name of the module. For instance: Assignments"/> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="create-module" layout:fragment="dialog"> + <form th:action="@{/module}" method="post" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Create module</h2> + </div> + <button class="hidden:from_small">Create</button> </div> - <div class="col-md-6"> - <button type="submit" class="btn btn-primary ctrl-enter-submit float-right"> - Create new module - </button> - <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script> + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <input name="edition" type="hidden" th:value="${edition.id}" /> + + <div class="flex_column gap-1"> + <label class="form__label" for="create-module-name">Name</label> + <input + class="textfield" + id="create-module-name" + name="name" + placeholder="Module name" + data-style="outlined" + required /> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create module</span> + </button> + </div> </div> - </div> - </form> -</section> -</body> + </form> + </dialog> </html> diff --git a/src/main/resources/templates/module/edit.html b/src/main/resources/templates/module/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..26a0d37d06b189daecadc29447301a20ae14b9cc --- /dev/null +++ b/src/main/resources/templates/module/edit.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="edit-module" layout:fragment="dialog"> + <form th:action="@{/module}" th:method="patch" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Edit module</h2> + </div> + <button class="hidden:from_small">Edit</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <input name="module" type="hidden" /> + + <div class="flex_column gap-1"> + <label class="form__label" for="edit-module-name">Name</label> + <input + class="textfield" + id="edit-module-name" + name="name" + placeholder="Module name" + data-style="outlined" + required /> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit module</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/module/remove.html b/src/main/resources/templates/module/remove.html index e2ad302af292b3b86984c7ea78c14c1ec57da098..6a843f47751b83207a431f9793c9d20c5a01f8b9 100644 --- a/src/main/resources/templates/module/remove.html +++ b/src/main/resources/templates/module/remove.html @@ -1,47 +1,50 @@ -<!-- - - 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 layout:fragment="subcontent"> - <div class="page-sub-header"> - <h3>Remove Module</h3> - </div> - - <form th:action="@{/module/{id}/remove(id=${_module.id})}" - class="form-horizontal" - method="post"> - Are you sure you want to remove <strong - th:text="${'module #' + _module.id + ' (' + _module.name + ')'}"></strong>? - <div class="text-center"> - <button class="btn btn-danger">Delete this Module</button> - <small>or <a - th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small> - </div> - </form> -</section> -</body> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="remove-module" layout:fragment="dialog"> + <form th:action="@{/module}" th:method="delete" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Delete module</h2> + </div> + <button class="hidden:from_small">Delete</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-6"> + <input name="id" type="hidden" /> + + <p class="font-400"> + Are you sure you want to delete + <span id="remove-module-name"></span> + ? + </p> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + data-type="negative"> + <span class="margin_right-1 fa-solid fa-trash-alt"></span> + <span>Delete module</span> + </button> + </div> + </div> + </form> + </dialog> </html> diff --git a/src/main/resources/templates/pagination.html b/src/main/resources/templates/pagination.html index e9ff0a1e557e26b4e498ef5ab927a6ba707c0360..41562f90d559ad243faeb640e91163a7de02e295 100644 --- a/src/main/resources/templates/pagination.html +++ b/src/main/resources/templates/pagination.html @@ -1,74 +1,85 @@ -<!-- - - 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"> - -<!--@thymesVar id="page" type="org.springframework.data.domain.Page<java.lang.Object>"--> -<!--@thymesVar id="size" type="java.lang.Integer"--> -<!--@thymesVar id="url" type="java.lang.String"--> - -<body> -<nav th:if="${page.getTotalPages() > 1}" aria-label="Page navigation" th:fragment="pagination (page, size)" th:with="url=${#httpServletRequest.requestURI}"> - <ul class="pagination"> - <li th:classappend="${not page.isFirst()} ? '' : 'disabled'" class="page-item"> - <span th:if="${page.isFirst()}" class="page-link">«</span> - - <a th:if="${!page.isFirst()}" th:href="@{${url}(page=0, size=${page.getSize()})}" class="page-link">«</a> - </li> - - <th:block th:if="${page.getTotalPages() < size}"> - <li th:each="pageNumber : ${#numbers.sequence(0, page.getTotalPages()-1)}" - th:classappend="${pageNumber == page.getNumber()} ? 'active' : ''" class="page-item"> - <span th:if="${pageNumber == page.getNumber()}" th:text="${pageNumber+1}" class="page-link">1</span> - <a th:if="${pageNumber != page.getNumber()}" th:text="${pageNumber+1}" - th:href="@{${url}(page=${pageNumber}, size=${page.getSize()})}" class="page-link">1</a> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <nav + aria-label="Page navigation" + layout:fragment="pagination(page, size)" + class="pagination" + th:data-page="${page.number}" + th:data-page-size="${page.size}"> + <ul role="list" th:if="${page.getTotalPages() > 1}"> + <li th:data-disabled="${page.isFirst()}" class="page__item"> + <span th:if="${page.isFirst()}" class="page__link">«</span> + <button th:unless="${page.isFirst()}" data-page="0" class="page__link"> + « + </button> </li> - </th:block> - <th:block th:unless="${page.getTotalPages() < size}"> - <th:block th:if="${page.getNumber() > size}"> - <li class="page-item"><a href="#" class="page-link">...</a></li> + <th:block th:if="${page.totalPages < size}"> + <li + th:each="pageNumber : ${#numbers.sequence(0, page.totalPages-1)}" + th:data-active="${pageNumber == page.number}" + class="page__item"> + <span + th:if="${pageNumber == page.number}" + th:text="${pageNumber+1}" + class="page__link"></span> + <button + th:unless="${pageNumber == page.number}" + th:text="${pageNumber+1}" + th:data-page="${pageNumber}" + class="page__link"></button> + </li> </th:block> - <th:block th:each="pageNumber : ${#numbers.sequence(page.getNumber() - size + 1, page.getNumber() + size - 1)}"> - <th:block th:if="${pageNumber >= 0 and pageNumber <= page.getTotalPages()-1}"> - <li th:classappend="${pageNumber == page.getNumber()} ? 'active' : ''" class="page-item"> - <span th:if="${pageNumber == page.getNumber()}" th:text="${pageNumber+1}" class="page-link">1</span> - <a th:if="${pageNumber != page.getNumber()}" th:text="${pageNumber+1}" - th:href="@{${url}(page=${pageNumber}, size=${page.getSize()})}" class="page-link">1</a> - </li> + <th:block th:unless="${page.totalPages < size}"> + <th:block th:if="${page.number > size}"> + <li class="page__item"><a href="#" class="page__link">...</a></li> </th:block> - </th:block> - <th:block th:if="${page.getNumber() + size <= page.getTotalPages()-1}"> - <li class="page-item"><a href="#" class="page-link">...</a></li> + <th:block + th:each="pageNumber : ${#numbers.sequence(page.number - size + 1, page.number + size - 1)}"> + <th:block th:if="${pageNumber >= 0 and pageNumber <= page.totalPages-1}"> + <li th:data-active="${pageNumber == page.number}" class="page__item"> + <span + th:if="${pageNumber == page.number}" + th:text="${pageNumber+1}" + class="page__link"></span> + <button + th:unless="${pageNumber == page.number}" + th:text="${pageNumber+1}" + th:data-page="${pageNumber}" + class="page__link"></button> + </li> + </th:block> + </th:block> + + <th:block th:if="${page.number + size <= page.totalPages-1}"> + <li class="page__item"><a href="#" class="page__link">...</a></li> + </th:block> </th:block> - </th:block> - <li th:classappend="${not page.isLast()} ? '' : 'disabled'" class="page-item"> - <span th:if="${page.isLast()}" class="page-link">»</span> + <li th:data-disabled="${page.isLast()}" class="page__item"> + <span th:if="${page.isLast()}" class="page__link">»</span> + <button + th:unless="${page.isLast()}" + class="page__link" + th:data-page="${page.totalPages-1}"> + » + </button> + </li> + </ul> - <a th:if="${!page.isLast()}" - th:href="@{${url}(page=${page.getTotalPages()-1}, size=${page.getSize()})}" class="page-link">»</a> - </li> - </ul> -</nav> -</body> + <script> + $(() => { + $(".pagination button.page__link").click(function () { + let url = new URL(window.location); + url.searchParams.set("page", $(this).data("page")); + window.location = url.href; + }); + }); + </script> + </nav> </html> diff --git a/src/main/resources/templates/participant/add.html b/src/main/resources/templates/participant/add.html new file mode 100644 index 0000000000000000000000000000000000000000..4def89c9fa22f87b5a45f3e8968702f87a2242cc --- /dev/null +++ b/src/main/resources/templates/participant/add.html @@ -0,0 +1,237 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="add-participants" layout:fragment="dialog"> + <div class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Add participants</h2> + </div> + <button class="hidden:from_small" data-submit="small">Add</button> + </div> + + <div class="dialog__body"> + <div class="flex margin_bottom-7 | flex_column:small"> + <nav data-tabs aria-label="Add participant"> + <ul + class="nav_list" + style="--align: center; --width: 12rem" + role="list" + data-style="outlined"> + <li data-active="true" data-view="add-single-participant"> + <button>Single participant</button> + </li> + <li data-active="false" data-view="add-multiple-participants"> + <button>Multiple participants</button> + </li> + <li data-active="false" data-view="import-participants"> + <button>Import participant</button> + </li> + </ul> + </nav> + + <form + id="add-single-participant" + th:action="@{/participant/single}" + method="post" + class="flex_column" + style="min-width: 16rem"> + <input type="hidden" name="edition" th:value="${edition.id}" /> + <div class="flex_column gap-0"> + <label class="form__label" for="single-participant-id"> + Participant + </label> + <input + class="textfield" + placeholder="Username, email, or student number" + name="identifier" + id="single-participant-id" + data-style="outlined" + required /> + </div> + <div class="flex_column gap-0"> + <label class="form__label" for="single-participant-role">Role</label> + <select + id="single-participant-role" + name="role" + data-style="outlined" + data-select + required> + <option value="STUDENT" selected>Student</option> + <option value="TA">TA</option> + <option value="HEAD_TA">Head TA</option> + <option value="TEACHER">Teacher</option> + </select> + </div> + + <button type="submit" class="hidden"></button> + </form> + + <form + id="add-multiple-participants" + th:action="@{/participant/multiple}" + method="post" + class="hidden flex_column" + style="min-width: 16rem"> + <input type="hidden" name="edition" th:value="${edition.id}" /> + <div class="flex_column gap-0"> + <label class="form__label" for="multiple-participant-role">Role</label> + <select + id="multiple-participant-role" + name="role" + data-style="outlined" + data-select + required> + <option value="STUDENT" selected>Student</option> + <option value="TA">TA</option> + <option value="HEAD_TA">Head TA</option> + <option value="TEACHER">Teacher</option> + </select> + </div> + + <div class="flex_column gap-0"> + <label class="form__label" for="multiple-participant-ids"> + Participants + </label> + <textarea + id="multiple-participant-ids" + name="identifiers" + class="textfield" + placeholder="Enter a list of usernames, emails, or student numbers" + rows="7" + data-style="outlined" + style="resize: none" + required></textarea> + </div> + + <button type="submit" class="hidden"></button> + </form> + + <form + id="import-participants" + th:action="@{/participant/import}" + method="post" + enctype="multipart/form-data" + class="hidden flex_column"> + <input type="hidden" name="edition" th:value="${edition.id}" /> + <div class="flex_column gap-1"> + <span class="font-300">The file should have the following format:</span> + <div + class="flex gap-0" + style=" + background-color: var(--neutral-1000); + border: 1px solid var(--neutral-600); + "> + <div + class="flex_column gap-0" + style=" + border-right: 1px solid var(--neutral-600); + background-color: var(--neutral-700); + color: var(--neutral-200); + padding: 0.5rem 0.25rem 0.5rem 1rem; + "> + <code>1</code> + <code>2</code> + <code>3</code> + </div> + <div class="flex_column gap-0 padding-3"> + <code>user,role</code> + <code>user,role</code> + <code>user,role</code> + </div> + </div> + <span class="font-300 text_muted"> + ' + <code>user</code> + ' can be a username, email, or student number. + </span> + <span class="font-300 text_muted"> + Valid roles are: student, ta, head_ta, and teacher. + </span> + </div> + + <div class="flex_column gap-0"> + <label class="form__label" for="participant-import-file"> + Participants + </label> + <input + id="participant-import-file" + name="file" + type="file" + data-file + data-style="outlined" + data-placeholder="Browse..." + required + accept="text/csv" /> + </div> + + <button type="submit" class="hidden"></button> + </form> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + id="add-participant-submit" + class="button padding_inline-5 padding_block-2" + data-style="outlined"> + <span data-submit="icon" class="margin_right-1 fa-solid fa-plus"></span> + <span data-submit="full">Add participant</span> + </button> + </div> + </div> + </div> + + <script> + $(() => { + const viewData = { + "add-single-participant": { + full: "Add participant", + small: "Add", + icon: "fa-plus", + }, + "add-multiple-participants": { + full: "Add participants", + small: "Add", + icon: "fa-plus", + }, + "import-participants": { + full: "Import participant", + small: "Import", + icon: "fa-file-import", + }, + }; + $("[data-view]").click(function () { + const view = $(this).data("view"); + $("[data-submit]").each(function (i, elem) { + const type = $(this).data("submit"); + if (type === "icon") { + $(elem).removeClass( + ...Object.keys(viewData).map(v => viewData[v][type]) + ); + $(elem).addClass(viewData[view][type]); + } else { + $(elem).text(viewData[view][type]); + } + }); + }); + $("#add-participant-submit").click(() => + $("#add-participants form:not(.hidden) button[type='submit']").click() + ); + }); + </script> + </dialog> +</html> diff --git a/src/main/resources/templates/participant/change_role.html b/src/main/resources/templates/participant/change_role.html new file mode 100644 index 0000000000000000000000000000000000000000..f504302e53bacfe5a926bbb590cd4d0b7c5cffaa --- /dev/null +++ b/src/main/resources/templates/participant/change_role.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="change-role" layout:fragment="dialog"> + <form th:action="@{/participant}" th:method="patch" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2> + Change + <span id="change-role-name"></span> + role + </h2> + </div> + <button class="hidden:from_small">Change</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <input name="edition" type="hidden" th:value="${edition.id}" /> + <input name="person" type="hidden" /> + + <div class="flex_column gap-1"> + <label class="form__label" for="change-role-role">Role</label> + <select id="change-role-role" name="role" data-select data-style="outlined"> + <option value="STUDENT" selected>Student</option> + <option value="TA">TA</option> + <option value="HEAD_TA">Head TA</option> + <option value="TEACHER">Teacher</option> + </select> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Change role</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/participant/remove.html b/src/main/resources/templates/participant/remove.html new file mode 100644 index 0000000000000000000000000000000000000000..38b1af63cb1b9c4daa6ba7e54022018875300804 --- /dev/null +++ b/src/main/resources/templates/participant/remove.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="remove-participant" layout:fragment="dialog"> + <form th:action="@{/participant}" th:method="delete" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Remove participant</h2> + </div> + <button class="hidden:from_small">Remove</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-6"> + <input name="edition" type="hidden" th:value="${edition.id}" /> + <input name="person" type="hidden" /> + + <p class="font-400"> + Are you sure you want to remove + <span id="remove-participant-name"></span> + from this edition? + </p> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + type="submit" + data-style="outlined" + data-type="negative"> + <span class="margin_right-1 fa-solid fa-minus"></span> + <span>Remove participant</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/question/create.html b/src/main/resources/templates/question/create.html new file mode 100644 index 0000000000000000000000000000000000000000..45848b4b6dab7a6a886383c6b7c8bc81a5070611 --- /dev/null +++ b/src/main/resources/templates/question/create.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="create-question" layout:fragment="dialog"> + <form th:action="@{/question}" method="post" class="dialog"> + <input type="hidden" name="edition.id" th:value="${edition.id}" /> + + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Create question</h2> + </div> + <button class="hidden:from_small">Create</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <div class="flex_column gap-1"> + <label for="question-question" class="form__label">Question</label> + <input + class="textfield" + id="question-question" + name="question" + placeholder="Question" + data-style="outlined" + required /> + </div> + <div class="flex_column gap-1"> + <label for="question-answer" class="form__label">Answer</label> + <input + class="textfield" + id="question-answer" + name="answer" + placeholder="Answer" + data-style="outlined" + required /> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + type="submit"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create question</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/question/edit.html b/src/main/resources/templates/question/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..581afdf2dbaa803af70373eab3d3e3084f2fb41c --- /dev/null +++ b/src/main/resources/templates/question/edit.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="edit-question" layout:fragment="dialog"> + <form th:action="@{/question}" th:method="patch" class="dialog"> + <input type="hidden" id="edit-question-id" name="questionId" /> + <input type="hidden" name="edition.id" th:value="${edition.id}" /> + + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Edit question</h2> + </div> + <button class="hidden:from_small">Edit</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <div class="flex_column gap-1"> + <label for="edit-question-question" class="form__label">Question</label> + <input + class="textfield" + id="edit-question-question" + name="question" + placeholder="Question" + data-style="outlined" + required /> + </div> + <div class="flex_column gap-1"> + <label for="edit-question-answer" class="form__label">Answer</label> + <input + class="textfield" + id="edit-question-answer" + name="answer" + placeholder="Answer" + data-style="outlined" + required /> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-pencil-alt"></span> + <span>Edit question</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/question/remove.html b/src/main/resources/templates/question/remove.html new file mode 100644 index 0000000000000000000000000000000000000000..5aaaa18680cadab4c26f59882602d57561a893ad --- /dev/null +++ b/src/main/resources/templates/question/remove.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="remove-question" layout:fragment="dialog"> + <form th:action="@{/question}" th:method="delete" class="dialog"> + <input type="hidden" name="editionId" th:value="${edition.id}" /> + <input type="hidden" id="remove-question-id" name="questionId" /> + + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Delete question</h2> + </div> + <button class="hidden:from_small">Delete</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-6"> + <p class="font-400">Are you sure you want to delete this question?</p> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + data-type="negative"> + <span class="margin_right-1 fa-solid fa-trash-alt"></span> + <span>Delete question</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/request/actions/approve.html b/src/main/resources/templates/request/actions/approve.html new file mode 100644 index 0000000000000000000000000000000000000000..1f28f68f5702af0d3b8ef64e5890e9fb743c1371 --- /dev/null +++ b/src/main/resources/templates/request/actions/approve.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="approve-request" layout:fragment="dialog"> + <form th:action="@{/request/approve}" method="post" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Approve request</h2> + </div> + <button class="hidden:from_small">Approve</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <input name="request" type="hidden" /> + + <div class="flex_column gap-1"> + <label class="form__label" for="approve-request-reason">Reason</label> + <textarea + class="textfield" + id="approve-request-reason" + name="reasonForAssistant" + placeholder="Reason for approval" + data-style="outlined" + rows="3" + style="resize: none"></textarea> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-check"></span> + <span>Approve request</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/request/actions/feedback.html b/src/main/resources/templates/request/actions/feedback.html new file mode 100644 index 0000000000000000000000000000000000000000..4435915a859ff017f418c65c341ca529572ea121 --- /dev/null +++ b/src/main/resources/templates/request/actions/feedback.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="request-feedback" layout:fragment="dialog"> + <div class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Give feedback</h2> + </div> + <button class="hidden:from_small">Save</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <div class="flex_column gap-1"> + <label class="form__label" for="feedback-assistant">Assistant</label> + <select + id="feedback-assistant" + name="assistant" + data-select + data-style="outlined"> + <option + th:each="assistant, iter : ${assistants}" + th:selected="${iter.index == 0}" + th:value="${assistant.id}" + th:text="${assistant.displayName}"></option> + </select> + </div> + + <form + th:each="assistant, iter : ${assistants}" + class="flex_column" + th:classappend="${iter.index == 0} ? '' : 'hidden'" + th:action="@{/request/{requestId}/feedback/{assistantId}(requestId=${request.id}, assistantId=${assistant.id})}" + method="post" + id="feedback-form" + th:with="feedback = ${@feedbackRepository.findById(request.id, assistant.id).orElse(null)}" + th:data-assistant="${assistant.id}"> + <div class="flex_column gap-1"> + <label class="form__label" for="feedback-score">Score</label> + <input type="hidden" id="feedback-score" name="rating" /> + <div class="stars" id="feedback-stars"> + <button + type="button" + value="1" + th:data-filled="${feedback == null} ? false : ${feedback.rating >= 1}"> + <span class="fa-solid fa-star"></span> + <span class="fa-regular fa-star"></span> + </button> + <button + type="button" + value="2" + th:data-filled="${feedback == null} ? false : ${feedback.rating >= 2}"> + <span class="fa-solid fa-star"></span> + <span class="fa-regular fa-star"></span> + </button> + <button + type="button" + value="3" + th:data-filled="${feedback == null} ? false : ${feedback.rating >= 3}"> + <span class="fa-solid fa-star"></span> + <span class="fa-regular fa-star"></span> + </button> + <button + type="button" + value="4" + th:data-filled="${feedback == null} ? false : ${feedback.rating >= 4}"> + <span class="fa-solid fa-star"></span> + <span class="fa-regular fa-star"></span> + </button> + <button + type="button" + value="5" + th:data-filled="${feedback == null} ? false : ${feedback.rating >= 5}"> + <span class="fa-solid fa-star"></span> + <span class="fa-regular fa-star"></span> + </button> + </div> + </div> + <div class="flex_column gap-1"> + <label class="form__label" for="feedback-text">Feedback</label> + <textarea + id="feedback-text" + class="textfield" + name="feedback" + maxlength="250" + th:placeholder="|Leave feedback about ${assistant.displayName}|" + style="resize: none" + rows="3" + data-style="outlined" + th:text="${feedback?.feedback}"></textarea> + </div> + </form> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + id="submit-feedback"> + <span class="margin_right-1 fa-solid fa-star"></span> + <span>Give feedback</span> + </button> + </div> + </div> + </div> + + <script> + $("#feedback-stars button").click(function () { + $("#feedback-stars button").each((i, e) => { + $(e).attr("data-filled", $(this).val() >= $(e).val()); + }); + $("#feedback-score").val($(this).val()); + $(this).blur(); + }); + $("#feedback-assistant").change(function () { + $("[data-assistant]").addClass("hidden"); + $(`[data-assistant=${$(this).val()}]`).removeClass("hidden"); + }); + $("#submit-feedback").click(() => $("#feedback-form:not(.hidden)").submit()); + </script> + </dialog> +</html> diff --git a/src/main/resources/templates/request/actions/forward.html b/src/main/resources/templates/request/actions/forward.html new file mode 100644 index 0000000000000000000000000000000000000000..bd96d159f59a56a67296e7f509331196f9fe1277 --- /dev/null +++ b/src/main/resources/templates/request/actions/forward.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="forward-request" layout:fragment="dialog"> + <form th:action="@{/request/forward}" method="post" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Forward request</h2> + </div> + <button class="hidden:from_small">Forward</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <input name="request" type="hidden" /> + + <div class="flex_column gap-1"> + <label class="form__label" for="forward-request-assistant"> + Forward to + </label> + <select + id="forward-request-assistant" + name="assistant" + data-select + data-style="outlined" + required> + <option value="-1" selected>Anyone but me</option> + <option + th:each="assistant : ${assistants}" + th:value="${assistant.id}" + th:text="${assistant.displayName}"></option> + </select> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="forward-request-reason">Reason</label> + <textarea + class="textfield" + id="forward-request-reason" + name="reasonForAssistant" + placeholder="Reason for forwarding" + data-style="outlined" + rows="3" + style="resize: none" + required></textarea> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-share"></span> + <span>Forward request</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/request/actions/reject.html b/src/main/resources/templates/request/actions/reject.html new file mode 100644 index 0000000000000000000000000000000000000000..3fe8d455ab929e1182440278e10f728af6e5d8a9 --- /dev/null +++ b/src/main/resources/templates/request/actions/reject.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog id="reject-request" layout:fragment="dialog"> + <form th:action="@{/request/reject}" method="post" class="dialog"> + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Reject request</h2> + </div> + <button class="hidden:from_small">Reject</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <input name="request" type="hidden" /> + + <div class="flex_column gap-1"> + <label class="form__label" for="reject-request-assistant-reason"> + Reason for assistant + </label> + <textarea + class="textfield" + id="reject-request-assistant-reason" + name="reasonForAssistant" + placeholder="Reason for rejection" + data-style="outlined" + rows="3" + style="resize: none" + required></textarea> + </div> + + <div class="flex_column gap-1"> + <label class="form__label" for="reject-request-student-reason"> + Reason for student + </label> + <textarea + class="textfield" + id="reject-request-student-reason" + name="reasonForStudent" + placeholder="Reason for rejection" + data-style="outlined" + rows="3" + style="resize: none" + required></textarea> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button class="button padding_inline-5 padding_block-2" data-style="outlined"> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Reject request</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/request/entry.html b/src/main/resources/templates/request/entry.html new file mode 100644 index 0000000000000000000000000000000000000000..5ce1c20c2022a833921a29dde9999414925d86da --- /dev/null +++ b/src/main/resources/templates/request/entry.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <tr + th:id="|request-${request.id}-row|" + layout:fragment="row(current)" + th:data-request="${request.id}" + data-selectable + th:data-current="${current}"> + <td class="flex align_items_center hidden:from_small" style="--gap: 6px"> + <button class="visually_hidden" th:data-request="${request.id}"></button> + <span + th:text="|${request.requestType.displayName()} for ${request.edition.course.name} by ${request.requester.displayName}|"></span> + <span + class="chip" + th:data-type="${request.eventInfo.status.colourClass}" + data-status + th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span> + </td> + <td class="hidden:small"> + <button class="visually_hidden" th:data-request="${request.id}"></button> + <span + class="chip" + th:data-type="${request.eventInfo.status.colourClass}" + data-status + th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span> + </td> + <td class="hidden:small"> + <span th:if="${request instanceof T(nl.tudelft.queue.model.SelectionRequest)}"> + Selection + </span> + <span + th:unless="${request instanceof T(nl.tudelft.queue.model.SelectionRequest)}" + th:text="${request.requestType.displayName()}"></span> + </td> + <td class="hidden:small" th:text="${request.requester.displayName}"></td> + <td + class="hidden:small" + th:text="|${request.room.building.name} - ${request.room.name}|"></td> + <td + class="hidden:small" + th:text="|${request.assignment.module.name} - ${request.assignment.name}|"></td> + <td class="hidden:small" th:text="${request.edition.course.name}"></td> + <td + class="hidden:small" + th:text="${current} ? '' : ${request.eventInfo.assignedTo?.displayName}"></td> + </tr> + + <script + layout:fragment="template" + id="request-entry-template" + type="text/x-handlebars-template"> + <tr id="request-{{id}}-row" data-request="{{id}}" data-selectable> + <td class="flex align_items_center hidden:from_small" style="--gap: 6px;"> + <button class="visually_hidden" data-request="{{id}}"></button> + <span> + {{requestTypeDisplayName}} for {{courseName}} by {{requestedBy}} + </span> + <span class="chip" data-type="regular"> + {{status}} + </span> + </td> + <td class="hidden:small"> + <button class="visually_hidden" data-request="{{request.id}}"></button> + <span class="chip" data-type="regular"> + {{statusDisplayName}} + </span> + </td> + <td class="hidden:small"> + <!-- <span th:if="${request instanceof T(nl.tudelft.queue.model.SelectionRequest)}">Selection</span>--> + <span> + {{requestTypeDisplayName}} + </span> + </td> + <td class="hidden:small"> + {{requestedBy}} + </td> + <td class="hidden:small"> + {{buildingName}} - {{roomName}} + </td> + <td class="hidden:small"> + {{moduleName}} - {{assignmentName}} + </td> + <td class="hidden:small"> + {{courseName}} + </td> + <td class="hidden:small"></td> + </tr> + </script> +</html> diff --git a/src/main/resources/templates/request/filters.html b/src/main/resources/templates/request/filters.html new file mode 100644 index 0000000000000000000000000000000000000000..4359bf781527cb399164645b9ce73bb8c42cfd91 --- /dev/null +++ b/src/main/resources/templates/request/filters.html @@ -0,0 +1,166 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog + id="filters" + layout:fragment="dialog(showSession, showAssignment, showRoom, showAssigned, showStatus, showType)"> + <form th:action="@{/filter}" method="post" class="dialog"> + <input type="hidden" name="return-path" th:value="${returnPath}" /> + + <div class="dialog__title"> + <div class="flex align_items_center gap-5"> + <button + class="hidden:from_small fa-solid fa-xmark" + type="button" + data-cancel></button> + <h2>Filter requests</h2> + </div> + <button class="hidden:from_small" name="filter-submit">Filter</button> + </div> + + <div class="dialog__body"> + <div class="flex_column margin_bottom-7"> + <div> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + name="filter-clear"> + <span class="margin_right-1 fa-solid fa-filter-circle-xmark"></span> + <span>Clear filters</span> + </button> + </div> + + <div + th:if="not ${labs?.isEmpty() ?: true} and ${showLabs != false}" + class="flex_column gap-1"> + <label class="form__label" for="lab-select">Session</label> + <select + id="lab-select" + name="labs" + data-select + multiple + data-style="outlined" + data-placeholder="None selected"> + <option + th:each="lab : ${labs}" + th:value="${lab.id}" + th:selected="${filter.labs.contains(lab.id)}" + th:text="${lab.name}"></option> + </select> + </div> + + <div + th:if="not ${assignments?.isEmpty() ?: true} and ${showAssignments != false}" + class="flex_column gap-1"> + <label class="form__label" for="assignment-select">Assignment</label> + <select + id="assignment-select" + name="assignments" + data-select + multiple + data-style="outlined" + data-placeholder="None selected"> + <option + th:each="assignment : ${assignments}" + th:value="${assignment.id}" + th:selected="${filter.assignments.contains(assignment.id)}" + th:text="|${assignment.module.name} - ${assignment.name}|"></option> + </select> + </div> + + <div + th:if="not ${rooms?.isEmpty() ?: true} and ${showRooms != false}" + class="flex_column gap-1"> + <label class="form__label" for="room-select">Room</label> + <select + id="room-select" + name="rooms" + data-select + multiple + data-style="outlined" + data-placeholder="None selected"> + <option + th:each="room : ${rooms}" + th:value="${room.id}" + th:selected="${filter.rooms.contains(room.id)}" + th:text="|${room.building.name} - ${room.name}|"></option> + </select> + </div> + + <div + th:if="not ${assignments?.isEmpty() ?: true} and ${showAssigned != false}" + class="flex_column gap-1"> + <label class="form__label" for="assigned-select">Assigned</label> + <select + id="assigned-select" + name="assigned" + data-select + multiple + data-style="outlined" + data-placeholder="None selected"> + <option + th:each="assistant : ${assistants}" + th:value="${assistant.id}" + th:selected="${filter.assigned.contains(assistant.id)}" + th:text="${assistant.displayName}"></option> + </select> + </div> + + <div th:if="${showStatus != false}" class="flex_column gap-1"> + <label class="form__label" for="status-select">Status</label> + <select + id="status-select" + name="requestStatuses" + data-select + multiple + data-style="outlined" + data-placeholder="None selected"> + <option + th:each="status : ${T(nl.tudelft.queue.model.enums.RequestStatus).values()}" + th:value="${status}" + th:selected="${filter.requestStatuses.contains(status)}" + th:text="#{|request.status.${status.name().toLowerCase()}|}"></option> + </select> + </div> + + <div th:if="${showType != false}" class="flex_column gap-1"> + <label class="form__label" for="request-type-select">Type</label> + <select + id="request-type-select" + name="requestTypes" + data-select + multiple + data-style="outlined" + data-placeholder="None selected"> + <option + th:each="type : ${T(nl.tudelft.queue.model.enums.RequestType).values()}" + th:value="${type}" + th:selected="${filter.requestTypes.contains(type)}" + th:text="${type.displayName()}"></option> + </select> + </div> + </div> + + <div class="flex space_between span-2 | hidden:small"> + <button + class="button padding_inline-5 padding_block-2" + type="button" + data-style="outlined" + data-cancel> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Cancel</span> + </button> + <button + class="button padding_inline-5 padding_block-2" + data-style="outlined" + name="filter-submit"> + <span class="margin_right-1 fa-solid fa-filter"></span> + <span>Filter</span> + </button> + </div> + </div> + </form> + </dialog> +</html> diff --git a/src/main/resources/templates/request/list.html b/src/main/resources/templates/request/list.html index 9a1e67069eec968122872aef9c834d6ca01a0f8c..3c1314e4a3dc1a22c1a9988b7c19dda715f05b85 100644 --- a/src/main/resources/templates/request/list.html +++ b/src/main/resources/templates/request/list.html @@ -1,118 +1,155 @@ -<!-- - - 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="~{layout}"> - -<!--@thymesVar id="filter" type="nl.tudelft.queue.dto.util.RequestTableFilterDTO"--> - -<!--@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="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="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.RequestViewDTO>"--> -<!--@thymesVar id="requestCounts" type="java.util.Map<java.lang.Long, java.lang.Long>"--> - -<head> - <title>Requests</title> - - <script 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"/> -</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 active">Requests</li> - </ol> - </nav> - - <div class="row"> - <h1 class="col-12">Requests</h1> - </div> - - <div class="row mb-2"> - <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" - th:id="|get-next-${lab.id}|" - th:if="${lab.isActiveOrGracePeriod}" - th:classappend="${requestCounts.getOrDefault(lab.id, 0) == 0} ? 'disabled' : ''" - th:href="@{/requests/next/{labId}(labId = ${lab.id})}"> - <span th:text="${@thymeleafConfig.isTheDay()} ? 'Pop from' : 'Get next for'"></span> - <th:block th:text="${lab.name}">Lab name</th:block> - <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" - class="btn btn-sm btn-success ml-4">Unstack me please</button> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <head> + <title th:text="Requests"></title> + + <script type="text/javascript" src="/webjars/handlebars/handlebars.min.js"></script> + </head> + + <div layout:fragment="breadcrumbs" class="hidden"></div> + + <th:block layout:fragment="content"> + <h1 class="title margin_bottom-5">Requests</h1> + + <div class="flex_column"> + <div> + <button + class="button padding_inline-5 padding_block-3" + data-style="floating" + data-dialog="filters"> + <span class="margin_right-1 fa-solid fa-filter"></span> + <span th:text="|Filters (${filter.countActiveFilters()})|"></span> + </button> + </div> + + <form + th:unless="${filter.isEmpty()}" + class="flex gap-3" + style="flex-wrap: wrap" + th:action="@{/filter}" + method="post"> + <input type="hidden" name="return-path" value="/requests" /> + <input type="hidden" name="filter-update" value="" /> + <button + th:each="lab : ${filter.labs}" + class="chip" + data-type="action" + name="labs" + th:value="${#strings.listJoin(filter.labs.?[#this != #root.lab], ',')}"> + <span class="fa-solid fa-filter"></span> + <span th:text="${labs.^[id == #root.lab].name}"></span> + <span class="fa-solid fa-xmark"></span> + </button> + <button + th:each="assignment : ${filter.assignments}" + class="chip" + data-type="action" + name="assignments" + th:value="${#strings.listJoin(filter.assignments.?[#this != #root.assignment], ',')}"> + <span class="fa-solid fa-filter"></span> + <span + th:with="a = ${assignments.^[id == #root.assignment]}" + th:text="|${a.module.name} - ${a.name}|"></span> + <span class="fa-solid fa-xmark"></span> + </button> + <button + th:each="room : ${filter.rooms}" + class="chip" + data-type="action" + name="rooms" + th:value="${#strings.listJoin(filter.rooms.?[#this != #root.room], ',')}"> + <span class="fa-solid fa-filter"></span> + <span + th:with="r = ${rooms.^[id == #root.room]}" + th:text="|${r.building.name} - ${r.name}|"></span> + <span class="fa-solid fa-xmark"></span> + </button> + <button + th:each="assigned : ${filter.assigned}" + class="chip" + data-type="action" + name="assigned" + th:value="${#strings.listJoin(filter.assigned.?[#this != #root.assigned], ',')}"> + <span class="fa-solid fa-filter"></span> + <span + th:text="|Assigned to: ${assistants.^[id == #root.assigned].displayName}|"></span> + <span class="fa-solid fa-xmark"></span> + </button> + <button + th:each="status : ${filter.requestStatuses}" + class="chip" + data-type="action" + name="requestStatuses" + th:value="${#strings.listJoin(filter.requestStatuses.?[#this != #root.status], ',')}"> + <span class="fa-solid fa-filter"></span> + <span th:text="#{|request.status.${status.name().toLowerCase()}|}"></span> + <span class="fa-solid fa-xmark"></span> + </button> + <button + th:each="requestType : ${filter.requestTypes}" + class="chip" + data-type="action" + name="requestTypes" + th:value="${#strings.listJoin(filter.requestTypes.?[#this != #root.requestType], ',')}"> + <span class="fa-solid fa-filter"></span> + <span th:text="${requestType.displayName()}"></span> + <span class="fa-solid fa-xmark"></span> + </button> </form> - <form th:if="${@thymeleafConfig.isTheDay() && !@requestTableService.partakes()}" - method="post" th:action="@{/requests/iloveallqueuedevelopers}"> - <button type="submit" - class="btn btn-sm btn-success ml-4">Stack me please</button> - </form> - </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 class="flex gap-3" style="flex-wrap: wrap"> + <button + th:each="lab : ${labs}" + th:id="|get-next-${lab.id}|" + class="button padding_inline-5 padding_block-3" + data-style="floating" + th:disabled="${requestCounts[lab.id] == 0}" + th:data-lab="${lab.id}"> + <span class="margin_right-1 fa-solid fa-hand"></span> + <!-- prettier-ignore --> + <span th:text="|Get next for ${lab.name} (|"></span> + <span data-count th:text="${requestCounts[lab.id]}"></span> + <span>)</span> + </button> + </div> + + <div class="flex_column"> + <th:block layout:replace="~{request/table :: table}"></th:block> + <th:block + layout:replace="~{pagination :: pagination(page=${requests}, size=3)}"></th:block> + </div> </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> - - <div class="justify-content-center"> - <th:block th:replace="pagination :: pagination (page=${requests}, size=3)"> - </th:block> - </div> -</section> -</body> + </th:block> + + <th:block layout:fragment="overlays" th:with="returnPath = '/requests'"> + <th:block + layout:replace="~{request/filters :: dialog(showAssigned=${false}, showStatus=${false})}"></th:block> + <th:block layout:replace="~{request/actions/approve :: dialog}"></th:block> + <th:block layout:replace="~{request/actions/reject :: dialog}"></th:block> + <th:block layout:replace="~{request/actions/forward :: dialog}"></th:block> + </th:block> + + <th:block layout:fragment="script"> + <script src="/js/request_table.js"></script> + <script> + $(() => { + $("[data-lab]").click(function () { + $.get(`/requests/next/${$(this).data("lab")}`, html => { + const element = $(html).filter((i, e) => $(e).is("dialog")); + if (element.is("dialog")) { + $(".page").append(element); + openDialog(element.attr("id")); + configureDialog(element); + } else { + window.location.reload(); + } + }); + }); + }); + </script> + </th:block> </html> - diff --git a/src/main/resources/templates/request/table.html b/src/main/resources/templates/request/table.html new file mode 100644 index 0000000000000000000000000000000000000000..d18146284ad0f7051fad8d00ee7cabaeb2a9debc --- /dev/null +++ b/src/main/resources/templates/request/table.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <head> + <title th:text="Requests"></title> + </head> + + <th:block layout:fragment="table(showAssistants)"> + <table id="request-table" class="rounded shadow table"> + <thead> + <tr class="heading"> + <th class="hidden:from_small">Requests</th> + <th class="hidden:small">Status</th> + <th class="hidden:small">Type</th> + <th class="hidden:small">Request by</th> + <th class="hidden:small">Room</th> + <th class="hidden:small">Assignment</th> + <th class="hidden:small">Course</th> + <th th:if="${showAssistants}" class="hidden:small">Assigned to</th> + <th th:unless="${showAssistants}" class="hidden:small">Forwarded by</th> + </tr> + </thead> + <tbody> + <th:block th:each="request : ${currentRequests}"> + <th:block layout:replace="~{request/entry :: row(current=true)}"></th:block> + </th:block> + <th:block th:each="request : ${requests}"> + <th:block layout:replace="~{request/entry :: row(current=false)}"></th:block> + </th:block> + </tbody> + </table> + + <script> + $(() => { + $("[data-request]").click(function () { + $.get(`/request/${$(this).data("request")}`, html => { + const element = $(html).filter((i, e) => $(e).is("dialog")); + $(".page").append(element); + openDialog(element.attr("id")); + configureDialog(element); + }); + }); + }); + </script> + + <th:block layout:replace="~{request/entry :: template}"></th:block> + </th:block> +</html> diff --git a/src/main/resources/templates/request/view.html b/src/main/resources/templates/request/view.html index 800a77656b39bf1a318975a15204c6912921e307..cdf18cbe13fe38a68c0685f450371dd3b96ad08c 100644 --- a/src/main/resources/templates/request/view.html +++ b/src/main/resources/templates/request/view.html @@ -1,132 +1,219 @@ -<!-- - - 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="~{layout}"> - -<!--@thymesVar id="request" type="nl.tudelft.queue.dto.view.RequestViewDTO"--> -<!--@thymesVar id="prevRequests" type="java.util.List<nl.tudelft.queue.dto.view.RequestViewDTO>"--> - -<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"--> -<!--@thymesVar id="assistant" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"--> - -<head> - <title th:text="${'Request #' + request.id}"></title> - - <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"/> -</head> - -<body> -<section layout:fragment="~{breadcrumb}"> - <nav role="navigation" class="breadcrumbs"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li> - <li class="breadcrumb-item"><a th:href="@{/editions}">Courses</a></li> - <li class="breadcrumb-item active" th:text="${'Request #' + request.id}"></li> - </ol> - </nav> -</section> - -<section layout:fragment="content"> - <th:block layout:fragment="claim-button"> - </th:block> - - <div class="row"> - <div class="col-sm-4"> - <th:block layout:fragment="feedback"> - </th:block> - - <th:block layout:fragment="history"> - </th:block> - </div> - - <div class="col-sm-8"> - <th:block layout:fragment="request-info"> - </th:block> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <dialog + th:id="|request-${request.id}|" + layout:fragment="dialog" + data-closable + data-remove-on-close + th:with="assistants = ${@feedbackService.assistantsInvolvedInRequest(request.id)}"> + <div class="dialog"> + <div class="flex_column gap-3"> + <div class="hidden:small"> + <button class="button fa-solid fa-xmark" data-style="text" data-cancel></button> + </div> + + <div class="flex align_items_center:from_small | flex_column:small"> + <div class="dialog__title"> + <div class="flex"> + <button + class="hidden:from_small fa-solid fa-xmark" + data-cancel></button> + <h2 + th:text="|${request.requestType.displayName()} by ${request.requester.displayName}|"></h2> + </div> + </div> + <span + class="chip hidden:small" + th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span> + </div> + </div> + + <div class="dialog__body flex_column"> + <form + th:if="${@permissionService.canPickRequest(request.id)}" + th:action="@{/request/{requestId}/pick(requestId=${request.id})}" + method="post" + id="claim-request"> + <button + class="button padding_inline-5 padding_block-3" + data-style="outlined" + type="submit"> + <span>Claim request</span> + </button> + </form> + + <div class="flex | flex_column:medium"> + <div class="flex_column"> + <div class="flex_column gap-0 align_items_start hidden:from_small"> + <span class="label">Status</span> + <span + class="font-400" + th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Course</span> + <span class="font-400" th:text="${request.edition.course.name}"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Session</span> + <span class="font-400" th:text="${request.session.name}"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Assignment</span> + <span + class="font-400" + th:text="|${request.assignment.module.name} - ${request.assignment.name}|"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Room</span> + <span + class="font-400" + th:text="|${request.room.building.name} - ${request.room.name}|"></span> + </div> + + <div class="flex_column gap-0"> + <span class="label">Question</span> + <p + class="font-400" + style="max-width: 80ch" + th:text="${request.question}"></p> + </div> + + <div th:if="${request.comment}" class="flex_column gap-0"> + <span class="label">Comment</span> + <p + class="font-400" + style="max-width: 80ch" + th:text="${request.comment}"></p> + </div> + </div> + + <div class="flex_column gap-0"> + <span class="label">History</span> + <ul class="history flex_column gap-3"> + <li + class="margin_left-5" + th:data-type="${event.iconClass}" + th:each="event : ${request.events}" + th:unless="${event instanceof T(nl.tudelft.queue.dto.view.events.RequestPickedEventViewDTO)}"> + <div class="flex_column gap-0"> + <p + class="font-400" + style="max-width: 80ch" + th:if="${@permissionService.canViewRequestAssistantReason(request.id)}" + th:text="${event.descriptionForAssistant}"></p> + <p + class="font-400" + style="max-width: 80ch" + th:unless="${@permissionService.canViewRequestAssistantReason(request.id)}" + th:text="${event.description}"></p> + <span + class="font-300" + th:text="${#temporals.format(event.timestamp, 'MMM dd, yyyy ''at'' HH:mm')}"></span> + </div> + </li> + </ul> + </div> + </div> + + <div + class="flex_column align_items_start | align_items_stretch:small" + th:if="${@permissionService.canGiveFeedback(request.id) and not assistants.isEmpty()}"> + <button + class="button padding_inline-5 padding_block-3" + data-style="outlined" + data-dialog="request-feedback"> + <span class="margin_right-1 fa-solid fa-star"></span> + <span>Give feedback</span> + </button> + </div> + + <div th:if="${@permissionService.canFinishRequest(request.id)}" class="grid gap-3"> + <button + class="button padding_inline-5 padding_block-3" + data-style="outlined" + data-type="positive" + data-dialog="approve-request" + th:data-request="${request.id}"> + <span class="margin_right-1 fa-solid fa-check"></span> + <span>Approve</span> + </button> + + <button + class="button padding_inline-5 padding_block-3" + data-style="outlined" + data-type="negative" + data-dialog="reject-request" + th:data-request="${request.id}"> + <span class="margin_right-1 fa-solid fa-xmark"></span> + <span>Reject</span> + </button> + + <button + class="button padding_inline-5 padding_block-3" + data-style="outlined" + data-dialog="forward-request" + th:data-request="${request.id}"> + <span class="margin_right-1 fa-solid fa-share"></span> + <span>Forward</span> + </button> + + <form + class="flex_column" + th:action="@{/request/{id}/not-found(id=${request.id})}" + method="get"> + <button + class="button padding_inline-5 padding_block-3" + data-style="outlined"> + <span class="margin_right-1 fa-solid fa-magnifying-glass"></span> + <span>Not found</span> + </button> + </form> + </div> + </div> + + <th:block layout:replace="~{request/actions/feedback :: dialog}"></th:block> </div> - <script src="/js/map_loader.js"></script> - <script type="text/javascript" th:inline="javascript"> - const roomId = /*[[${request.room}]]*/ 0; - updateRequestInfo(roomId); + <script> + $("[data-dialog='approve-request']").click(function () { + $("#approve-request input[name='request']").val($(this).data("request")); + openDialog("approve-request"); + }); + $("[data-dialog='reject-request']").click(function () { + $("#reject-request input[name='request']").val($(this).data("request")); + openDialog("reject-request"); + }); + $("[data-dialog='forward-request']").click(function () { + $("#forward-request input[name='request']").val($(this).data("request")); + openDialog("forward-request"); + }); + $("[data-dialog='request-feedback']").click(function () { + openDialog("request-feedback"); + }); + configureSelect($("#feedback-assistant")); + + $("#claim-request").submit(function (event) { + event.preventDefault(); + $.post($(this).attr("action"), html => { + const element = $(html).filter((i, e) => $(e).is("dialog")); + $(this).closest("dialog").remove(); + if (element.is("dialog")) { + $(".page").append(element); + openDialog(element.attr("id")); + configureDialog(element); + } else { + window.location.reload(); + } + }); + }); </script> - - <style> - .btn-group.feedback > .btn.focus { - transition: .5s; - outline: none !important; - box-shadow: none; - } - - .btn-group.feedback > .btn.active:nth-child(1) { - background: #C23A2C !important; - border: 1px solid #AD3427 !important; - } - - .btn-group.feedback > .btn.active:nth-child(2) { - background: #F78921 !important; - border: 1px solid #F67C09 !important; - } - - .btn-group.feedback > .btn.active:nth-child(3) { - background: #EAD234 !important; - border: 1px solid #E8CD1D !important; - } - - .btn-group.feedback > .btn.active:nth-child(4) { - background: #76D142 !important; - border: 1px solid #68C931 !important; - } - - .btn-group.feedback > .btn.active:nth-child(5) { - background: #489B52 !important; - border: 1px solid #408949 !important; - } - - .btn-group.feedback > .btn:hover:nth-child(1) { - background: #C23A2C !important; - border: 1px solid #AD3427 !important; - } - - .btn-group.feedback > .btn:hover:nth-child(2) { - background: #F78921 !important; - border: 1px solid #F67C09 !important; - } - - .btn-group.feedback > .btn:hover:nth-child(3) { - background: #EAD234 !important; - border: 1px solid #E8CD1D !important; - } - - .btn-group.feedback > .btn:hover:nth-child(4) { - background: #76D142 !important; - border: 1px solid #68C931 !important; - } - - .btn-group.feedback > .btn:hover:nth-child(5) { - background: #489B52 !important; - border: 1px solid #408949 !important; - } - </style> - </div> -</section> -</body> + </dialog> </html> diff --git a/src/main/resources/templates/request/view/lab.html b/src/main/resources/templates/request/view/lab.html index 08ed3184beb6f95db14558a202765ff8d420ddbf..4f0c18139ae8b97428d884b62eec92535d8293fd 100644 --- a/src/main/resources/templates/request/view/lab.html +++ b/src/main/resources/templates/request/view/lab.html @@ -1,45 +1,6 @@ -<!-- - - 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="~{request/view}"> - -<body> -<th:block layout:fragment="claim-button"> - <th:block th:replace="request/view/components/claim-button :: claim-button"> - </th:block> -</th:block> - -<th:block layout:fragment="feedback"> - <th:block th:replace="request/view/components/feedback :: feedback"> - </th:block> -</th:block> - -<th:block layout:fragment="history"> - <th:block th:replace="request/view/components/history :: history"> - </th:block> -</th:block> - -<th:block layout:fragment="request-info"> - <th:block th:replace="request/view/components/lab-request-info :: request-info"> - </th:block> -</th:block> -</body> -</html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{request/view}"></html> diff --git a/src/main/resources/templates/shared-edition/view.html b/src/main/resources/templates/shared-edition/view.html index 6ba75a80df706ad97aaff8a645d3d82eb100ae55..45be1e134d8a50b20c05779baa510292c5454af9 100644 --- a/src/main/resources/templates/shared-edition/view.html +++ b/src/main/resources/templates/shared-edition/view.html @@ -1,94 +1,84 @@ <!DOCTYPE html> -<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" - layout:decorate="~{layout}"> -<body> - <section layout:fragment="content"> - <section layout:fragment="breadcrum"> - <nav role="navigation" class="breadcrumbs"> - <ol class="breadcrumb"> - <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li> - <li class="breadcrumb-item"> - <a th:href="@{/}">My Courses</a> - </li> - <li class="breadcrumb-item active" th:text="${collection.getName()}">TI1316</li> - </ol> - </nav> - </section> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{container}"> + <head> + <title th:text="${collection.name}"></title> + </head> - <div class="controls" th:if="${@permissionService.canManageSharedSessions(collection.id)}"> - <div class="controls_buttons"> - <button class="btn btn-primary" - onclick="showDialog('create-shared-session-dialog')">Create shared - session</button> + <div layout:fragment="breadcrumbs" class="breadcrumbs"> + <a th:href="@{/}">My courses</a> + <span>></span> + <a th:text="${collection.name}" th:href="@{/shared-edition/{id}(id=${collection.id})}"></a> + </div> + + <th:block layout:fragment="content"> + <div + class="flex space_between align_items_center margin_bottom-3:from_small | flex_column:small align_items_stretch:small"> + <h1 class="title" th:text="${collection.name}"></h1> + <div class="flex gap-3 | flex_column:small margin_bottom-5:small gap-5:small"> + <!-- <button--> + <!-- th:if="${@permissionService.canManageEdition(edition.id)}"--> + <!-- class="button padding_block-3 padding_inline-5"--> + <!-- data-style="floating"--> + <!-- data-dialog="edit-edition">--> + <!-- <span class="margin_right-1 fa-solid fa-pencil-alt"></span>--> + <!-- <span>Edit course edition</span>--> + <!-- </button>--> </div> - <div th:replace="~{shared-edition/view/create-session-dialog :: overlay}"></div> </div> - <div class="tabs"> - <button id="sessions-tab" class="active tab" onclick="switchTab('sessions')">Sessions - </button> - <button id="editions-tab" class="tab" onclick="switchTab('editions')">Editions</button> - <button id="staff-tab" class="tab" onclick="switchTab('staff')">Staff</button> - </div> + <!-- <div--> + <!-- th:if="${@permissionService.canManageModules(edition.id)} and ${edition.modules.isEmpty()}"--> + <!-- class="banner margin_bottom-3"--> + <!-- data-type="error">--> + <!-- <span class="fa-solid fa-exclamation-circle"></span>--> + <!-- <p>--> + <!-- This edition does not have any modules. Without modules, it is not possible to--> + <!-- create sessions.--> + <!-- </p>--> + <!-- </div>--> + <!-- <div--> + <!-- th:if="${@permissionService.canManageAssignments(edition.id)} and ${not edition.modules.isEmpty() and assignments.isEmpty()}"--> + <!-- class="banner margin_bottom-3"--> + <!-- data-type="error">--> + <!-- <span class="fa-solid fa-exclamation-circle"></span>--> + <!-- <p>--> + <!-- This edition does not have any assignments. Without assignments, it is not possible--> + <!-- to create sessions.--> + <!-- </p>--> + <!-- </div>--> - <div th:replace="~{shared-edition/view/session-list :: tab}"></div> + <nav aria-label="Edition tabs" class="tabs margin_bottom-5"> + <a + th:data-active="${#request.requestURI.matches('.*/labs.*')}" + th:href="@{/shared-edition/{id}/labs(id=${collection.id})}"> + <!-- TODO assignments empty check--> + <span class="fa-solid fa-calendar-alt"></span> + <span>Sessions</span> + </a> + <a + th:data-active="${#request.requestURI.matches('.*/participants.*')}" + th:href="@{/shared-edition/{id}/participants(id=${collection.id})}" + th:if="${@permissionService.canManageSharedParticipants(collection.id)}"> + <span class="fa-solid fa-user"></span> + <span>Participants</span> + </a> + <a + th:data-active="${#request.requestURI.matches('.*/editions.*')}" + th:href="@{/shared-edition/{id}/editions(id=${collection.id})}"> + <span class="fa-solid fa-sitemap"></span> + <span>Courses</span> + </a> + </nav> - <div id="editions" class="hidden"> - <table class="table"> - <thead> - <tr> - <th>Course edition</th> - <th>Course code</th> - <th>Teachers</th> - </tr> - </thead> - <tbody> - <tr th:each="edition : ${editions}"> - <td><a 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:with="teacher = ${editionTeachers[edition.id].toString()}" - th:text="${teacher.substring(1, teacher.length() - 1)}"></td> - </tr> - </tbody> - </table> - </div> - - <div id="staff" class="hidden"> - <table class="table"> - <thead> - <tr class="table_header"> - <th>Name</th> - <th>Role</th> - </tr> - </thead> - <tbody> - <tr th:each="instance : ${roles}"> - <td th:text="${instance.key.displayName}"></td> - <td> - <ul> - <li th:each="r : ${instance.getValue()}" - th:text="|${r.getType().getValue()} (${@editionCacheManager.getOrThrow(r.id.editionId).course.name})|"> - </li> - </ul> - </td> - </tr> - </tbody> - </table> - </div> + <th:block layout:fragment="edition-content"></th:block> + </th:block> - <script> - let currentTab = "sessions" - function switchTab(to) { - if (to !== currentTab) { - document.getElementById(to).classList.toggle("hidden"); - document.getElementById(currentTab).classList.toggle("hidden"); - document.getElementById(to + "-tab").classList.toggle("active"); - document.getElementById(currentTab + "-tab").classList.toggle("active"); - } - currentTab = to; - } - </script> - </section> -</body> + <th:block layout:fragment="overlays"> + <!-- <th:block layout:replace="~{edition/edit :: dialog}"></th:block>--> + <th:block layout:fragment="extra_overlays"></th:block> + </th:block> </html> diff --git a/src/main/resources/templates/shared-edition/view/editions.html b/src/main/resources/templates/shared-edition/view/editions.html new file mode 100644 index 0000000000000000000000000000000000000000..494e709f61b22e8017495b25a4bca1ffe2ef0f4a --- /dev/null +++ b/src/main/resources/templates/shared-edition/view/editions.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{shared-edition/view}"> + <th:block layout:fragment="edition-content"> + <table class="rounded shadow table"> + <thead> + <tr class="heading space_between"> + <th>Name</th> + <th>Course Code</th> + <th class="hidden:small">Teachers</th> + </tr> + </thead> + <tbody> + <tr th:each="edition : ${editions}"> + <td> + <a + class="link" + th:href="@{/edition/{id}(id=${edition.id})}" + th:text="|${edition.course.value().name} - ${edition.name}|"></a> + </td> + <td th:text="${edition.course.value().code}"></td> + <td + class="hidden:small" + th:text="${#strings.listJoin(teachers[edition.id].![person.value().displayName], ', ')}"></td> + </tr> + </tbody> + </table> + </th:block> + + <th:block layout:fragment="extra_overlays"></th:block> + + <script layout:fragment="script"></script> +</html> diff --git a/src/main/resources/templates/shared-edition/view/labs.html b/src/main/resources/templates/shared-edition/view/labs.html new file mode 100644 index 0000000000000000000000000000000000000000..9ede58c1379c8e46f6de6ab784c8bf048b1e49ee --- /dev/null +++ b/src/main/resources/templates/shared-edition/view/labs.html @@ -0,0 +1,111 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{shared-edition/view}"> + <th:block layout:fragment="edition-content"> + <div + class="flex space_between align_items_center margin_bottom-5 | flex_column:small align_items_stretch:small"> + <form id="filters" class="flex gap-3 | flex_column:small gap-5:small"> + <input type="hidden" name="collection" th:value="${collection.id}" /> + <select + name="queueSessionTypes" + aria-label="Session type" + data-select + multiple + data-placeholder="Session type" + data-style="floating"> + <option + th:each="type : ${T(nl.tudelft.queue.model.enums.QueueSessionType).values()}" + th:value="${type}" + th:selected="${param.queueSessionTypes?.toString()?.contains(type.name())} ?: false" + th:text="#{|lab.type.${type.name.toLowerCase()}|}"></option> + </select> + <select + name="modules" + aria-label="Module" + data-select + multiple + data-placeholder="Module" + data-style="floating"> + <option + th:each="module : ${allModules}" + th:value="${module.id}" + th:text="${module.name}" + th:selected="${param.modules?.contains(module.id.toString())} ?: false"></option> + </select> + </form> + <div + th:if="${@permissionService.canManageSharedSessions(collection.id)}" + class="flex gap-3"> + <button + class="button padding_block-3 padding_inline-5 | flex_grow:small" + data-style="floating"> + <span class="margin_right-1 fa-solid fa-file-export"></span> + <span>Export sessions</span> + </button> + <button + class="button padding_block-3 padding_inline-5 | flex_grow:small" + data-style="floating" + data-dialog="create-session"> + <span class="margin_right-1 fa-solid fa-plus"></span> + <span>Create session</span> + </button> + </div> + </div> + + <th:block + th:with="canManage = ${@permissionService.canManageSharedSessions(collection.id)}"> + <th:block + layout:replace="~{edition/view/labs_tables :: tables(shared=true)}"></th:block> + </th:block> + </th:block> + + <th:block layout:fragment="extra_overlays"> + <th:block layout:replace="~{lab/create :: dialog(shared=true)}"></th:block> + <th:block layout:replace="~{lab/remove :: dialog}"></th:block> + </th:block> + + <th:block layout:fragment="script"> + <th:block layout:replace="~{lab/create :: create_script}"></th:block> + + <script> + $(() => { + // Filter on change of sessions + $("#filters select[name='queueSessionTypes']").change(function () { + $("#filters").submit(); + }); + // Filter on change of modules + $("#filters select[name='modules']").change(function () { + $("#filters").submit(); + }); + // Do not reload page when submitting the filters + $("#filters").submit(function (event) { + event.preventDefault(); + const queueSessionTypes = $("select[name='queueSessionTypes']").val().join(","); + const modules = $("select[name='modules']").val().join(","); + window.history.replaceState( + null, + null, + `?queueSessionTypes=${queueSessionTypes}&modules=${modules}` + ); + $.ajax({ + url: `/shared-edition/${$("input[name='collection']").val()}/labs/list`, + type: "get", + data: { + queueSessionTypes: queueSessionTypes, + modules: modules, + }, + success: html => { + $("#labs").replaceWith($(html)); + $("#labs [data-dialog]").click(function () { + openDialog($(this).data("dialog")); + }); + }, + }); + }); + }); + </script> + </th:block> +</html> diff --git a/src/main/resources/templates/shared-edition/view/participants.html b/src/main/resources/templates/shared-edition/view/participants.html new file mode 100644 index 0000000000000000000000000000000000000000..b30df3994a849091825d4bbf05f1cf503607dc7c --- /dev/null +++ b/src/main/resources/templates/shared-edition/view/participants.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" + layout:decorate="~{shared-edition/view}"> + <th:block layout:fragment="edition-content"> + <form id="filters" class="flex_column gap-3 align_items_start margin_bottom-5"> + <input type="hidden" name="edition" th:value="${collection.id}" /> + <div class="searchbox" data-style="floating"> + <input + aria-label="Participant search" + type="text" + name="search" + placeholder="Search" + th:value="${param.search}" /> + <button class="fa-solid fa-search"></button> + </div> + <select + class="hidden" + aria-label="Role" + name="roles" + data-select + multiple + data-placeholder="Role" + data-style="floating"> + <option + th:selected="${param.roles?.toString()?.contains('TEACHER')} ?: false" + value="TEACHER"> + Teacher + </option> + <option + th:selected="${param.roles?.toString()?.contains('HEAD_TA')} ?: false" + value="HEAD_TA"> + Head TA + </option> + <option + th:selected="${param.roles?.toString()?.contains('TA')} ?: false" + value="TA"> + TA + </option> + <option + th:selected="${param.roles?.toString()?.contains('STUDENT')} ?: false" + value="STUDENT"> + Student + </option> + </select> + </form> + + <th:block layout:replace="~{shared-edition/view/participants_table :: table}"></th:block> + </th:block> + + <th:block layout:fragment="extra_overlays"></th:block> + + <script layout:fragment="script"> + $(() => { + // Filter on change of roles + $("select[name='roles']").change(function () { + $("#filters").submit(); + }); + // Filter when user stopped typing + let timeout = null; + $("input[name='search']").keydown(function () { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + $("#filters").submit(); + }, 250); + }); + // Do not reload page when submitting the filters + $("#filters").submit(function (event) { + event.preventDefault(); + const size = $(".pagination").data("page-size"); + const search = $("input[name='search']").val(); + const roles = $("select[name='roles']").val().join(","); + window.history.replaceState( + null, + null, + `?search=${search}&roles=${roles}&page=0&size=${size}` + ); + $.ajax({ + url: `/shared-edition/${$("input[name='edition']").val()}/participants/list`, + type: "get", + data: { + search: search, + roles: roles, + page: 0, + size: size, + }, + success: html => { + $("#participants").replaceWith($(html)); + $("#participants") + .find("[data-dialog]") + .click(function () { + openDialog($(this).data("dialog"), false); + }); + }, + }); + }); + }); + </script> +</html> diff --git a/src/main/resources/templates/shared-edition/view/participants_table.html b/src/main/resources/templates/shared-edition/view/participants_table.html new file mode 100644 index 0000000000000000000000000000000000000000..a8fb891c17716b27e23649eeda2e1bb42fb2a75c --- /dev/null +++ b/src/main/resources/templates/shared-edition/view/participants_table.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <div id="participants" class="flex_column gap-5" layout:fragment="table"> + <table class="rounded shadow table table__to_list"> + <thead> + <tr class="heading flex:small space_between"> + <th class="hidden:from_small flex space_between align_items_center"> + Participants + </th> + <th class="hidden:small">Name</th> + <th class="hidden:small">Roles</th> + </tr> + </thead> + <tbody> + <tr th:if="${roles.isEmpty()}"> + <td>No participants found</td> + <td></td> + <td></td> + </tr> + <tr + th:each="entry : ${roles}" + th:with="person = ${entry.key}, roles = ${entry.value}" + class="flex_column:small gap-0"> + <td + class="flex:small align_items_center padding_bottom-4:small" + style="--gap: 6px"> + <span th:text="${person.displayName}"></span> + <span + class="chip hidden:from_small" + th:each="role : ${roles}" + th:text="#{|role.${role.type.name().toLowerCase()}|}"></span> + </td> + <td class="hidden:small"> + <ul> + <li th:each="role : ${roles}"> + <span th:text="#{|role.${role.type.name().toLowerCase()}|}"></span> + <span + th:text="|(${@editionFinder.byId(role.id.editionId).course.value().name})|"></span> + </li> + </ul> + </td> + </tr> + </tbody> + </table> + + <th:block layout:replace="~{pagination :: pagination(page=${roles}, size=3)}"></th:block> + </div> +</html> diff --git a/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java b/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java index 55fbbcc6283c52aad6e60d55b2eb8c9cbb3d2b88..a618440a8cf183709dc4f8b44d77c5854753754d 100644 --- a/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java +++ b/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java @@ -29,7 +29,7 @@ public class QueueAssignmentCreateDTOTest extends CreateTest<AssignmentCreateDTO protected QueueAssignmentCreateDTO filledDTO() { return QueueAssignmentCreateDTO.builder() .name("Oei") - .moduleId(999L) + .module(999L) .description("This is a simple yet complicated description") .deadline(LocalDateTime.of(2020, 9, 9, 20, 9, 19)) .build(); @@ -39,7 +39,7 @@ public class QueueAssignmentCreateDTOTest extends CreateTest<AssignmentCreateDTO public List<Arguments> getters() { return List.of( of(QueueAssignmentCreateDTO::getName, AssignmentCreateDTO::getName), - of(QueueAssignmentCreateDTO::getModuleId, acd -> acd.getModule().getId()), + of(QueueAssignmentCreateDTO::getModule, acd -> acd.getModule().getId()), of(QueueAssignmentCreateDTO::getDeadline, AssignmentCreateDTO::getDeadline), of(QueueAssignmentCreateDTO::getDescription, acd -> acd.getDescription().getText())); } diff --git a/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java b/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java index 2f54aebb5bc6f3647b68845e257498b8306d2a7c..418eab571a9f93d9e5771232067626a266654fd5 100644 --- a/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java +++ b/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java @@ -28,7 +28,7 @@ public class QueueModuleCreateDTOTest extends CreateTest<ModuleCreateDTO, QueueM protected QueueModuleCreateDTO filledDTO() { return QueueModuleCreateDTO.builder() .name("This is a name for a module") - .editionId(78434L) + .edition(78434L) .build(); } @@ -36,6 +36,6 @@ public class QueueModuleCreateDTOTest extends CreateTest<ModuleCreateDTO, QueueM public List<Arguments> getters() { return List.of( of(QueueModuleCreateDTO::getName, ModuleCreateDTO::getName), - of(QueueModuleCreateDTO::getEditionId, mcd -> mcd.getEdition().getId())); + of(QueueModuleCreateDTO::getEdition, mcd -> mcd.getEdition().getId())); } } diff --git a/src/test/java/nl/tudelft/queue/service/CapacitySessionServiceTest.java b/src/test/java/nl/tudelft/queue/service/CapacitySessionServiceTest.java index 9d31e6434eb89c198bd8d21395525e9bacb11af6..56dd709a44d22fcea77a850c6dc90bbba80d4312 100644 --- a/src/test/java/nl/tudelft/queue/service/CapacitySessionServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/CapacitySessionServiceTest.java @@ -31,7 +31,7 @@ import java.util.stream.Collectors; import javax.transaction.Transactional; import nl.tudelft.labracore.api.dto.*; -import nl.tudelft.queue.cache.RoomCacheManager; +import nl.tudelft.labracore.lib.cache.RoomCacheManager; import nl.tudelft.queue.model.QSelectionRequest; import nl.tudelft.queue.model.Request; import nl.tudelft.queue.model.SelectionRequest; diff --git a/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java b/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java index 615dac96a298ff92aedcad0ac8977eafbe3dd4af..317f413e359f4de4c629197b4e1aa21529a0a1fa 100644 --- a/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java @@ -150,19 +150,19 @@ public class EditionServiceTest { @Test void studentsMatchingFilterChecksDisplayName() { - assertThat(es.studentsMatchingFilter(people, "en")) + assertThat(es.rolesMatchingFilter(people, "en")) .containsExactly(person2, person4); } @Test void studentsMatchingFilterChecksStudentNumber() { - assertThat(es.studentsMatchingFilter(people, "5")) + assertThat(es.rolesMatchingFilter(people, "5")) .containsExactly(person1, person3, person5); } @Test void studentsMatchingFilterChecksUsername() { - assertThat(es.studentsMatchingFilter(people, "p")) + assertThat(es.rolesMatchingFilter(people, "p")) .containsExactly(person1, person3); } diff --git a/src/test/java/nl/tudelft/queue/service/JitsiServiceTest.java b/src/test/java/nl/tudelft/queue/service/JitsiServiceTest.java index 0a60552b34cd518d3c03c4b26ba23482b94096d8..77b876d19ccff679dcd869795f2a29f2736b5f73 100644 --- a/src/test/java/nl/tudelft/queue/service/JitsiServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/JitsiServiceTest.java @@ -23,7 +23,7 @@ import static org.mockito.Mockito.when; import java.time.LocalDateTime; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; -import nl.tudelft.queue.cache.PersonCacheManager; +import nl.tudelft.labracore.lib.cache.PersonCacheManager; import nl.tudelft.queue.model.LabRequest; import nl.tudelft.queue.properties.JitsiProperties; diff --git a/src/test/java/nl/tudelft/queue/service/LabServiceTest.java b/src/test/java/nl/tudelft/queue/service/LabServiceTest.java index aca75ef83a729c7b2ceaf0f80512f8f244fcedb1..8419d3a7f61acd5f32a9f3c03b50b3df9f6417df 100644 --- a/src/test/java/nl/tudelft/queue/service/LabServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/LabServiceTest.java @@ -34,7 +34,7 @@ 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.labracore.lib.cache.SessionCacheManager; import nl.tudelft.queue.dto.create.constraints.ClusterConstraintCreateDTO; import nl.tudelft.queue.dto.create.embeddables.CapacitySessionConfigCreateDTO; import nl.tudelft.queue.dto.create.embeddables.LabRequestConstraintsCreateDTO; diff --git a/src/test/java/nl/tudelft/queue/service/MailServiceTest.java b/src/test/java/nl/tudelft/queue/service/MailServiceTest.java index b7940f39e1c8e4c99ed5cbbf088829daa2e0cac3..407bff4e93ac2d72c28cc69d9129d0553ef96d2b 100644 --- a/src/test/java/nl/tudelft/queue/service/MailServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/MailServiceTest.java @@ -22,8 +22,8 @@ import static org.mockito.Mockito.verify; import javax.transaction.Transactional; -import nl.tudelft.labracore.lib.security.user.DefaultRole; -import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.labracore.lib.api.DefaultRole; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.queue.dto.create.CourseRequestCreateDTO; import nl.tudelft.queue.properties.MailProperties; import nl.tudelft.queue.properties.QueueProperties; diff --git a/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java b/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java index 50d0e05c8b7bc223180b33ac1c5caf6ae87681f3..c1246b29e0bf7a9455b208ab26bd9a451d9109cd 100644 --- a/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java +++ b/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java @@ -31,7 +31,7 @@ import javax.transaction.Transactional; import nl.tudelft.labracore.api.StudentGroupControllerApi; import nl.tudelft.labracore.api.dto.*; -import nl.tudelft.queue.cache.SessionCacheManager; +import nl.tudelft.labracore.lib.cache.SessionCacheManager; import nl.tudelft.queue.dto.create.requests.SelectionRequestCreateDTO; import nl.tudelft.queue.model.QSelectionRequest; import nl.tudelft.queue.model.SelectionRequest; diff --git a/src/test/java/test/BaseMockConfig.java b/src/test/java/test/BaseMockConfig.java index e610300341c087d8c653db0817eaf32125ee8719..95e87a64bc92a910a869a894ed5adc15b2866e58 100644 --- a/src/test/java/test/BaseMockConfig.java +++ b/src/test/java/test/BaseMockConfig.java @@ -18,7 +18,7 @@ package test; import nl.tudelft.labracore.api.*; -import nl.tudelft.queue.cache.CourseCacheManager; +import nl.tudelft.labracore.lib.cache.CourseCacheManager; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Configuration; diff --git a/src/test/java/test/TestUserDetailsService.java b/src/test/java/test/TestUserDetailsService.java index 862d61e742a37b1d7e097de6b659cbf5fb5f4fc6..c6dcac5a13b32ac23c907c45ee9994b7020429c9 100644 --- a/src/test/java/test/TestUserDetailsService.java +++ b/src/test/java/test/TestUserDetailsService.java @@ -17,9 +17,9 @@ */ package test; +import nl.tudelft.labracore.lib.api.DefaultRole; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.LabradorUserDetails; -import nl.tudelft.labracore.lib.security.user.DefaultRole; -import nl.tudelft.labracore.lib.security.user.Person; import org.springframework.context.annotation.Primary; import org.springframework.security.core.userdetails.UserDetails; diff --git a/src/test/java/test/test/TestUserDetailsService.java b/src/test/java/test/test/TestUserDetailsService.java index 047ef459ae7045d14195b671e65e0de19e35b76a..d0ea52311179ed435a1d977c0ba56cf524c02a7e 100644 --- a/src/test/java/test/test/TestUserDetailsService.java +++ b/src/test/java/test/test/TestUserDetailsService.java @@ -18,9 +18,9 @@ package test.test; import nl.tudelft.labracore.api.dto.PersonSummaryDTO; +import nl.tudelft.labracore.lib.api.DefaultRole; +import nl.tudelft.labracore.lib.data.Person; import nl.tudelft.labracore.lib.security.LabradorUserDetails; -import nl.tudelft.labracore.lib.security.user.DefaultRole; -import nl.tudelft.labracore.lib.security.user.Person; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.DependsOn;