diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/DataLoader.java b/EEDU-Backend/src/main/java/de/gaz/eedu/DataLoader.java index 9f092702..7a94bf57 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/DataLoader.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/DataLoader.java @@ -1,5 +1,6 @@ package de.gaz.eedu; +import de.gaz.eedu.exception.EntityUnknownException; import de.gaz.eedu.user.AccountType; import de.gaz.eedu.user.UserEntity; import de.gaz.eedu.user.UserService; @@ -35,11 +36,7 @@ import java.util.Set; import java.util.stream.Collectors; -@Component -@RequiredArgsConstructor -@Slf4j -@Getter(AccessLevel.PROTECTED) -public class DataLoader implements CommandLineRunner +@Component @RequiredArgsConstructor @Slf4j @Getter(AccessLevel.PROTECTED) public class DataLoader implements CommandLineRunner { private final UserService userService; private final GroupService groupService; @@ -132,20 +129,20 @@ private void createDefaultGroup() private @NotNull ThemeEntity createDefaultTheme() { - ThemeCreateModel defaultDark = new ThemeCreateModel( - "defaultDark", + ThemeCreateModel defaultDark = new ThemeCreateModel("Dark", new byte[]{Byte.MIN_VALUE + 5, Byte.MIN_VALUE + 5, Byte.MIN_VALUE + 5}, new byte[]{Byte.MIN_VALUE + 10, Byte.MIN_VALUE + 10, Byte.MIN_VALUE + 10}); - ThemeCreateModel defaultLight = new ThemeCreateModel( - "defaultLight", + ThemeCreateModel defaultLight = new ThemeCreateModel("Light", new byte[]{Byte.MIN_VALUE + 255, Byte.MIN_VALUE + 255, Byte.MIN_VALUE + 255}, new byte[]{Byte.MIN_VALUE + 235, Byte.MIN_VALUE + 235, Byte.MIN_VALUE + 235}); + String defaultThemeName = "Dark"; + return getThemeService().createEntity(Set.of(defaultDark, defaultLight)).stream().filter(theme -> { // Dark will be set as default - return Objects.equals(theme.getName(), "defaultDark"); - }).findFirst().orElseThrow(); + return Objects.equals(theme.getName(), defaultThemeName); + }).findFirst().orElseThrow(() -> new EntityUnknownException(defaultThemeName)); } private @NotNull UserEntity createDefaultUser(@NotNull ThemeEntity themeEntity) diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/adapter/PeriodArgumentAdapter.java b/EEDU-Backend/src/main/java/de/gaz/eedu/adapter/PeriodArgumentAdapter.java index 4925ec4e..8ee6f869 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/adapter/PeriodArgumentAdapter.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/adapter/PeriodArgumentAdapter.java @@ -4,8 +4,6 @@ import jakarta.persistence.Converter; import org.jetbrains.annotations.Nullable; -import java.time.Duration; -import java.time.Instant; import java.time.Period; import java.util.Objects; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/blogging/PostCreateModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/blogging/PostCreateModel.java index 4f8e52d2..8ded21e5 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/blogging/PostCreateModel.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/blogging/PostCreateModel.java @@ -1,16 +1,8 @@ package de.gaz.eedu.blogging; import de.gaz.eedu.entity.model.CreationModel; -import de.gaz.eedu.file.FileCreateModel; -import jakarta.validation.constraints.Null; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.Unmodifiable; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; public record PostCreateModel(@NotNull String author, @NotNull String title, @Nullable String thumbnailURL, @NotNull String body, @NotNull String[] editPrivileges, @NotNull String[] tags) implements CreationModel diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/CourseController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/CourseController.java index 4f8b6ad0..3af98217 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/CourseController.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/CourseController.java @@ -26,7 +26,8 @@ public class CourseController extends EntityController setSubject(@PathVariable long course, @PathVariable String subject) { log.info("Received incoming request for setting the subject of course {} to {}.", course, subject); diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentController.java index 224fe553..5deaea05 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentController.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentController.java @@ -3,7 +3,6 @@ import de.gaz.eedu.course.appointment.entry.model.AppointmentEntryCreateModel; import de.gaz.eedu.course.appointment.entry.model.AppointmentEntryModel; import de.gaz.eedu.course.appointment.entry.model.AppointmentUpdateModel; -import de.gaz.eedu.course.appointment.entry.model.AssignmentInsightModel; import de.gaz.eedu.course.appointment.frequent.FrequentAppointmentEntity; import de.gaz.eedu.course.appointment.frequent.model.FrequentAppointmentCreateModel; import de.gaz.eedu.course.appointment.frequent.model.FrequentAppointmentModel; @@ -17,9 +16,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.Set; @@ -54,42 +51,6 @@ public class AppointmentController extends EntityController submitStatus(@PathVariable long appointment) - { - return ResponseEntity.ok(getService().getInsight(appointment).toArray(AssignmentInsightModel[]::new)); - } - - @PreAuthorize("hasRole('teacher')") - @GetMapping("/assignment/{appointment}/status/{user}") - public @NotNull ResponseEntity submitStatus(@PathVariable long appointment, @PathVariable long user) - { - ResponseEntity notFound = ResponseEntity.notFound().build(); - return getService().getInsight(appointment, user).map(ResponseEntity::ok).orElse(notFound); - } - - @PreAuthorize("hasRole('student')") - @GetMapping("/assignment/{appointment}/status") - public @NotNull ResponseEntity ownSubmitStatus(@AuthenticationPrincipal long userId, @PathVariable long appointment) - { - ResponseEntity notFound = ResponseEntity.notFound().build(); - return getService().getInsight(appointment, userId).map(ResponseEntity::ok).orElse(notFound); - } - - @DeleteMapping("/assignment/{appointment}/delete/{files}") - public @NotNull ResponseEntity deleteAssignment(@AuthenticationPrincipal long userId, @PathVariable long appointment, @PathVariable @NotNull String[] files) - { - return empty(getService().deleteAssignment(userId, appointment, files) ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR); - } - - @PostMapping("/assignment/{appointment}/submit") - public @NotNull ResponseEntity submitAssignment(@AuthenticationPrincipal long userId, @PathVariable long appointment, @NotNull @RequestPart("file") MultipartFile[] files) - { - getService().submitAssignment(userId, appointment, files); - return empty(HttpStatus.CREATED); - } - @PostMapping("/update/standalone/{appointment}") @PreAuthorize("hasRole('teacher') or hasRole('administrator')") public @NotNull ResponseEntity updateAppointment(@PathVariable long appointment, @NotNull @RequestBody AppointmentUpdateModel updateModel) { @@ -102,13 +63,12 @@ public class AppointmentController extends EntityController setAppointment(@PathVariable long course, @RequestBody @NotNull AppointmentEntryCreateModel... createModel) + @PutMapping("/{course}/schedule/standalone") @PreAuthorize("hasRole('teacher') or hasRole('administrator')") + public @NotNull ResponseEntity scheduleAppointment(@PathVariable long course, @RequestBody @NotNull AppointmentEntryCreateModel... createModel) { - List createdEntities = getService().createAppointment(course, Set.of(createModel)); return ResponseEntity.ok(createdEntities.toArray(AppointmentEntryModel[]::new)); } diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentService.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentService.java index 33d1f922..3e33ad0c 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentService.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentService.java @@ -4,6 +4,8 @@ import de.gaz.eedu.course.CourseRepository; import de.gaz.eedu.course.appointment.entry.AppointmentEntryEntity; import de.gaz.eedu.course.appointment.entry.AppointmentEntryRepository; +import de.gaz.eedu.course.appointment.entry.assignment.AssignmentCreateModel; +import de.gaz.eedu.course.appointment.entry.assignment.AssignmentInsightModel; import de.gaz.eedu.course.appointment.entry.model.*; import de.gaz.eedu.course.appointment.frequent.FrequentAppointmentEntity; import de.gaz.eedu.course.appointment.frequent.FrequentAppointmentRepository; @@ -14,8 +16,10 @@ import de.gaz.eedu.entity.EntityService; import de.gaz.eedu.exception.CreationException; import de.gaz.eedu.exception.EntityUnknownException; +import de.gaz.eedu.file.FileService; import de.gaz.eedu.user.UserEntity; import de.gaz.eedu.user.repository.UserRepository; +import io.jsonwebtoken.io.IOException; import jakarta.transaction.Transactional; import lombok.AccessLevel; import lombok.Getter; @@ -23,7 +27,9 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; @@ -39,6 +45,7 @@ @RequiredArgsConstructor public class AppointmentService extends EntityService { + private final FileService fileService; private final FrequentAppointmentRepository repository; @Getter(AccessLevel.PUBLIC) private final AppointmentEntryRepository entryRepository; @@ -113,9 +120,8 @@ private static boolean setAssignment(@NotNull AppointmentEntryEntity entity, @Nu Function equals = (room -> !Objects.equals(updateModel.room(), room.getId())); if (entity.getRoom().map(equals).orElseGet(() -> Objects.nonNull(updateModel.room()))) { - entity.setRoom( - Objects.isNull(updateModel.room()) ? null : - roomRepository.findById(updateModel.room()).orElseThrow(entityUnknown(updateModel.room())) + entity.setRoom(Objects.isNull(updateModel.room()) ? null : + roomRepository.findById(updateModel.room()).orElseThrow(entityUnknown(updateModel.room())) ); } @@ -217,6 +223,30 @@ private AppointmentEntryEntity getAppointmentEntry(long id) throws EntityUnknown return entryReference.orElseThrow(entityUnknown(id)); } + public @NotNull ResponseEntity downloadAssignments(long appointment, long user) + { + AppointmentEntryEntity entry = getEntryRepository().findById(appointment).orElseThrow(entityUnknown(appointment)); + return getFileService().zipAndSend(entry.loadAssignmentFiles(user)); + } + + public @NotNull ResponseEntity downloadAssignment(long appointment, long user, @NotNull String file) + { + try + { + AppointmentEntryEntity entry = getEntryRepository().findById(appointment).orElseThrow(entityUnknown(appointment)); + return getFileService().sendSingle(entry.loadAssignmentFile(user, file).orElseThrow(() -> + { + String message = String.format("Could not find file %s in appointment: %s", file, appointment); + return new IOException(message); + })); + } + catch (java.io.IOException exception) + { + String message = "An exception occurred while trying to prepare downloading assignment: %s"; + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, String.format(message, file), exception); + } + } + /** * Creates a list of {@link AppointmentEntryEntity} based on the given course and creation models. *

diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/AppointmentEntryEntity.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/AppointmentEntryEntity.java index 5ced4b79..06d7fc78 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/AppointmentEntryEntity.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/AppointmentEntryEntity.java @@ -5,23 +5,24 @@ import de.gaz.eedu.course.CourseEntity; import de.gaz.eedu.course.CourseService; import de.gaz.eedu.course.appointment.AppointmentService; +import de.gaz.eedu.course.appointment.entry.assignment.assessment.AssessmentEntity; +import de.gaz.eedu.course.appointment.entry.assignment.assessment.model.AssessmentModel; import de.gaz.eedu.course.appointment.entry.model.AppointmentEntryModel; -import de.gaz.eedu.course.appointment.entry.model.AssignmentCreateModel; -import de.gaz.eedu.course.appointment.entry.model.AssignmentInsightModel; -import de.gaz.eedu.course.appointment.entry.model.AssignmentModel; +import de.gaz.eedu.course.appointment.entry.assignment.AssignmentCreateModel; +import de.gaz.eedu.course.appointment.entry.assignment.AssignmentInsightModel; +import de.gaz.eedu.course.appointment.entry.assignment.AssignmentModel; import de.gaz.eedu.course.appointment.frequent.FrequentAppointmentEntity; import de.gaz.eedu.course.room.RoomEntity; import de.gaz.eedu.entity.model.EntityModelRelation; import de.gaz.eedu.file.FileEntity; import de.gaz.eedu.user.UserEntity; +import de.gaz.eedu.user.model.ReducedUserModel; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.hibernate.annotations.Cascade; -import org.hibernate.annotations.CascadeType; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,9 +40,8 @@ import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; -import java.util.Arrays; -import java.util.Objects; -import java.util.Optional; +import java.util.*; +import java.util.function.Predicate; @Slf4j @Entity @@ -55,8 +55,8 @@ public class AppointmentEntryEntity implements EntityModelRelation assessments = new HashSet<>(); + /** * This constructor creates a new instance of this entity. *

@@ -130,21 +133,41 @@ public AppointmentEntryEntity(long id) public @NotNull AssignmentInsightModel getInsight(@NotNull UserEntity user) { - String uploadPath = getUploadPath(user.getId()); - File file = new File(uploadPath); + File file = new File(getUploadPath(user.getId())); File[] files = file.listFiles(); + + AssessmentModel assessment = getAssessment(user).map(AssessmentEntity::toModel).orElse(null); + ReducedUserModel reducedUserModel = user.toReducedModel(); + if (!hasSubmitted(user) || !file.isDirectory() || Objects.isNull(files) || files.length == 0) { - return new AssignmentInsightModel(user.getLoginName(), false, new String[0]); + return new AssignmentInsightModel(reducedUserModel, false, new String[0], assessment); } String[] paths = Arrays.stream(files).map(File::getName).toArray(String[]::new); - return new AssignmentInsightModel(user.getLoginName(), true, paths); + return new AssignmentInsightModel(reducedUserModel, true, paths, assessment); + } + + public @NotNull File[] loadAssignmentFiles(long user) + { + return Objects.requireNonNullElse(new File(getUploadPath(user)).listFiles(), new File[0]); + } + + public @NotNull Optional loadAssignmentFile(long user, @NotNull String file) + { + File couldBe = loadFileSave(getUploadPath(user), file); + if(!couldBe.exists() || !couldBe.isDirectory() || !couldBe.canRead()) + { + return Optional.empty(); + } + + return Optional.of(couldBe); } // method not allowed when submitHomework is false // bad gateway when any file is malicious // bad request when + public void submitAssignment(long user, @NotNull MultipartFile... files) throws ResponseStatusException { if (!this.isAssignmentValid()) @@ -154,12 +177,12 @@ public void submitAssignment(long user, @NotNull MultipartFile... files) throws String uploadPath = getUploadPath(user); File[] file = new File(uploadPath).listFiles(); - if (file != null && (file.length + files.length) > 5) + if (file != null && (file.length + files.length) > 3) { throw new ResponseStatusException(HttpStatus.PAYLOAD_TOO_LARGE, "The maximum amount of files exceeded."); } - getCourse().getRepository().uploadBatch(uploadPath, files); + getCourse().getRepository().uploadBatch(uploadPath(user), files); log.info("User {} has uploaded files to appointment entry {}.", user, getId()); } @@ -183,6 +206,12 @@ public boolean deleteAssignment(long user, String @NotNull ... files) return allDeleted; } + private @NotNull Optional getAssessment(@NotNull UserEntity user) + { + Predicate userEquals = current -> Objects.equals(user, current.getUser()); + return getAssessments().stream().filter(userEquals).findFirst(); + } + private @NotNull String getUploadPath(long user) { FileEntity repository = getCourse().getRepository(); @@ -196,7 +225,8 @@ public boolean hasSubmitted(@NotNull UserEntity user) return false; } - return new File(getCourse().getRepository().getFilePath(uploadPath(user.getId()))).exists(); + File file = new File(getUploadPath(user.getId())); + return file.exists() && file.isDirectory(); } private @NotNull String uploadPath(long user) diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/AppointmentEntryRepository.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/AppointmentEntryRepository.java index ea57d7b7..777569a6 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/AppointmentEntryRepository.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/AppointmentEntryRepository.java @@ -13,9 +13,10 @@ public interface AppointmentEntryRepository extends JpaRepository findById(@NotNull Long id); } diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentController.java new file mode 100644 index 00000000..e5288835 --- /dev/null +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentController.java @@ -0,0 +1,77 @@ +package de.gaz.eedu.course.appointment.entry.assignment; + +import de.gaz.eedu.course.appointment.AppointmentService; +import de.gaz.eedu.course.appointment.entry.assignment.assessment.AssessmentService; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api/v1/course/appointment/assignment") +@RequiredArgsConstructor +@Getter(AccessLevel.PROTECTED) +public class AssignmentController +{ + private final AssessmentService service; + private final AppointmentService appointmentService; + + @GetMapping("/{appointment}/download/{user}") + public @NotNull ResponseEntity downloadAssignments(@PathVariable long appointment, @PathVariable long user) + { + return getAppointmentService().downloadAssignments(appointment, user); + } + + @GetMapping("/{appointment}/download/{user}/{file}") + public @NotNull ResponseEntity downloadAssignment(@PathVariable long appointment, @PathVariable long user, @PathVariable String file) + { + return getAppointmentService().downloadAssignment(appointment, user, file); + } + + @PreAuthorize("hasRole('teacher')") @GetMapping("/{appointment}/status/all") + public @NotNull ResponseEntity submitStatus(@PathVariable long appointment) + { + return ResponseEntity.ok(getAppointmentService().getInsight(appointment).toArray(AssignmentInsightModel[]::new)); + } + + @PreAuthorize("hasRole('teacher')") @GetMapping("/{appointment}/status/{user}") + public @NotNull ResponseEntity submitStatus(@PathVariable long appointment, @PathVariable long user) + { + ResponseEntity notFound = ResponseEntity.notFound().build(); + return getAppointmentService().getInsight(appointment, user).map(ResponseEntity::ok).orElse(notFound); + } + + @PreAuthorize("hasRole('student')") @GetMapping("/{appointment}/status") + public @NotNull ResponseEntity ownSubmitStatus(@AuthenticationPrincipal long userId, @PathVariable long appointment) + { + ResponseEntity notFound = ResponseEntity.notFound().build(); + return getAppointmentService().getInsight(appointment, userId).map(ResponseEntity::ok).orElse(notFound); + } + + @DeleteMapping("/{appointment}/delete/{files}") + public @NotNull ResponseEntity deleteAssignment(@AuthenticationPrincipal long userId, @PathVariable long appointment, @PathVariable @NotNull String[] files) + { + return ResponseEntity.status(getAppointmentService().deleteAssignment( + userId, + appointment, + files) ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR + ).build(); + } + + @PostMapping("/{appointment}/submit") + public @NotNull ResponseEntity submitAssignment(@AuthenticationPrincipal long userId, @PathVariable long appointment, @NotNull @RequestPart( + "file" + ) MultipartFile[] files) + { + getAppointmentService().submitAssignment(userId, appointment, files); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + +} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AssignmentCreateModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentCreateModel.java similarity index 94% rename from EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AssignmentCreateModel.java rename to EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentCreateModel.java index eada8f75..72fb6f2c 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AssignmentCreateModel.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentCreateModel.java @@ -1,4 +1,4 @@ -package de.gaz.eedu.course.appointment.entry.model; +package de.gaz.eedu.course.appointment.entry.assignment; import de.gaz.eedu.course.appointment.entry.AppointmentEntryEntity; import org.jetbrains.annotations.NotNull; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentInsightModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentInsightModel.java new file mode 100644 index 00000000..e45a2c5f --- /dev/null +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentInsightModel.java @@ -0,0 +1,13 @@ +package de.gaz.eedu.course.appointment.entry.assignment; + +import de.gaz.eedu.course.appointment.entry.assignment.assessment.model.AssessmentModel; +import de.gaz.eedu.user.model.ReducedUserModel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record AssignmentInsightModel( + @NotNull ReducedUserModel user, + boolean submitted, + @NotNull String[] files, + @Nullable AssessmentModel assessment +) {} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AssignmentModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentModel.java similarity index 87% rename from EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AssignmentModel.java rename to EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentModel.java index f18adaaf..dfe7b310 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AssignmentModel.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/AssignmentModel.java @@ -1,4 +1,4 @@ -package de.gaz.eedu.course.appointment.entry.model; +package de.gaz.eedu.course.appointment.entry.assignment; import org.jetbrains.annotations.NotNull; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentController.java new file mode 100644 index 00000000..cd786b9a --- /dev/null +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentController.java @@ -0,0 +1,64 @@ +package de.gaz.eedu.course.appointment.entry.assignment.assessment; + +import de.gaz.eedu.course.appointment.entry.assignment.assessment.model.AssessmentCreateModel; +import de.gaz.eedu.course.appointment.entry.assignment.assessment.model.AssessmentModel; +import de.gaz.eedu.entity.EntityController; +import de.gaz.eedu.exception.CreationException; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/course/appointment/assignment/assessment") +@RequiredArgsConstructor @Getter(AccessLevel.PROTECTED) +public class AssessmentController extends EntityController +{ + private final AssessmentService service; + + @PreAuthorize("hasRole('teacher')") + @PutMapping("/{assessment}/set/feedback/{feedback}") + public @NotNull ResponseEntity setFeedback(@PathVariable long assessment, @PathVariable @NotNull String feedback) + { + return ResponseEntity.ok(getService().setFeedback(assessment, feedback)); + } + + @PreAuthorize("hasRole('teacher')") + @PutMapping("/{assessment}/unset/feedback") + public @NotNull ResponseEntity unsetFeedback(@PathVariable long assessment) + { + return ResponseEntity.ok(getService().setFeedback(assessment, null)); + } + + @PreAuthorize("hasRole('teacher')") + @Override @PostMapping("/create") + public @NotNull ResponseEntity create(@NotNull @RequestBody AssessmentCreateModel[] model) throws CreationException + { + return super.create(model); + } + + @PreAuthorize("hasRole('teacher')") + @DeleteMapping("/delete/{id}") @Override + public @NotNull ResponseEntity delete(@NotNull @PathVariable Long[] id) + { + return super.delete(id); + } + + @PreAuthorize("hasRole('student')") + @GetMapping("/get/{appointment}") + public @NotNull ResponseEntity getOwnData(@NotNull @PathVariable Long appointment, @AuthenticationPrincipal long user) + { + return getData(appointment, user); + } + + @PreAuthorize("hasRole('teacher')") + @GetMapping("/get/{appointment}/{user}") + public @NotNull ResponseEntity getData(@NotNull @PathVariable Long appointment , @PathVariable long user) + { + return super.getData(getService().getId(user, appointment)); + } +} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentEntity.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentEntity.java new file mode 100644 index 00000000..6a96e6d7 --- /dev/null +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentEntity.java @@ -0,0 +1,41 @@ +package de.gaz.eedu.course.appointment.entry.assignment.assessment; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import de.gaz.eedu.course.appointment.entry.AppointmentEntryEntity; +import de.gaz.eedu.course.appointment.entry.assignment.assessment.model.AssessmentModel; +import de.gaz.eedu.entity.model.EntityModelRelation; +import de.gaz.eedu.user.UserEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class AssessmentEntity implements EntityModelRelation +{ + @Id private @NotNull Long id; + @JsonManagedReference @ManyToOne @Setter(AccessLevel.NONE) + + @JoinColumn(name = "appointment_id", referencedColumnName = "id", nullable = false) + private AppointmentEntryEntity appointment; + @ManyToOne @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false) @Setter(AccessLevel.NONE) + private UserEntity user; + + @Column(length = 200) private String feedback; + public AssessmentEntity(long id, @NotNull AppointmentEntryEntity appointment, @NotNull UserEntity user) + { + this.id = id; + this.appointment = appointment; + this.user = user; + } + + @Override public @NotNull AssessmentModel toModel() + { + return new AssessmentModel(getId(), getFeedback()); + } +} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentRepository.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentRepository.java new file mode 100644 index 00000000..5ffe410b --- /dev/null +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentRepository.java @@ -0,0 +1,15 @@ +package de.gaz.eedu.course.appointment.entry.assignment.assessment; + +import org.jetbrains.annotations.NotNull; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AssessmentRepository extends JpaRepository +{ + @Query("SELECT a FROM AssessmentEntity a LEFT JOIN FETCH a.appointment ap LEFT JOIN FETCH a.user u WHERE a.id = :id") + @Override @NotNull Optional findById(@NotNull Long id); +} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentService.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentService.java new file mode 100644 index 00000000..3f5e7f9e --- /dev/null +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/AssessmentService.java @@ -0,0 +1,78 @@ +package de.gaz.eedu.course.appointment.entry.assignment.assessment; + +import de.gaz.eedu.course.appointment.entry.AppointmentEntryEntity; +import de.gaz.eedu.course.appointment.entry.AppointmentEntryRepository; +import de.gaz.eedu.course.appointment.entry.assignment.assessment.model.AssessmentCreateModel; +import de.gaz.eedu.course.appointment.entry.assignment.assessment.model.AssessmentModel; +import de.gaz.eedu.entity.EntityService; +import de.gaz.eedu.exception.CreationException; +import de.gaz.eedu.user.UserEntity; +import de.gaz.eedu.user.repository.UserRepository; +import jakarta.transaction.Transactional; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +@Service @Getter(AccessLevel.PROTECTED) @RequiredArgsConstructor +public class AssessmentService extends EntityService +{ + private final AssessmentRepository repository; + private final AppointmentEntryRepository appointmentRepository; + private final UserRepository userRepository; + + @Contract(pure = true, value = "_, _ -> _") + private static long generateId(long entity, long user) + { + // okay so, this is a bit complex, so I'll break it down + // Firstly, I bitshift the appointmentEntryId into the lower 32 bits, because a long is 64 bits in total + + // Secondly, I shift the userid into the 32 remaining bits + // The value 0xFFFFFFFFFL is a 32-bit mask in hexadecimal format + + // The 0x indicates it's a hexadecimal number, where each F are 4 bits (there are 8), + // making up a total of 32 bits. The L suffix defines that the value is of type Long + + // This allows me to encode both the appointment id and the userid into a single id + return (entity << 32) | (user & 0xFFFFFFFFL); + } + + public long getId(long entity, long user) + { + return generateId(entity, user); + } + + @Transactional @Override + public @NotNull List createEntity(@NotNull Set model) throws CreationException + { + return saveEntity(model.stream().map(current -> { + long aId = current.appointment(); + AppointmentEntryEntity appointment = getAppointmentRepository().findById(aId).orElseThrow(entityUnknown(aId)); + UserEntity user = getUserRepository().findById(current.user()).orElseThrow(entityUnknown(current.user())); + return current.toEntity(new AssessmentEntity(generateId(appointment.getId(), user.getId()), appointment, user)); + }).toList()); + } + + @Transactional + public @NotNull AssessmentModel setFeedback(long assessment, @Nullable String feedback) + { + AssessmentEntity assessmentEntity = loadEntityByIDSafe(assessment); + + if (Objects.equals(assessmentEntity.getFeedback(), feedback)) + { + throw new ResponseStatusException(HttpStatus.CONFLICT); + } + + assessmentEntity.setFeedback(feedback); + return saveEntity(assessmentEntity).toModel(); + } +} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/model/AssessmentCreateModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/model/AssessmentCreateModel.java new file mode 100644 index 00000000..199f9333 --- /dev/null +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/model/AssessmentCreateModel.java @@ -0,0 +1,15 @@ +package de.gaz.eedu.course.appointment.entry.assignment.assessment.model; + +import de.gaz.eedu.course.appointment.entry.assignment.assessment.AssessmentEntity; +import de.gaz.eedu.entity.model.CreationModel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record AssessmentCreateModel(long appointment, long user, @Nullable String feedback) implements CreationModel +{ + @Override public @NotNull AssessmentEntity toEntity(@NotNull AssessmentEntity entity) + { + entity.setFeedback(feedback()); + return entity; + } +} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/model/AssessmentModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/model/AssessmentModel.java new file mode 100644 index 00000000..080e39b5 --- /dev/null +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/assignment/assessment/model/AssessmentModel.java @@ -0,0 +1,7 @@ +package de.gaz.eedu.course.appointment.entry.assignment.assessment.model; + +import de.gaz.eedu.entity.model.EntityModel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record AssessmentModel(@NotNull Long id, @Nullable String feedback) implements EntityModel {} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentEntryCreateModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentEntryCreateModel.java index 4845a14c..5e70df5e 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentEntryCreateModel.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentEntryCreateModel.java @@ -1,6 +1,7 @@ package de.gaz.eedu.course.appointment.entry.model; import de.gaz.eedu.course.appointment.entry.AppointmentEntryEntity; +import de.gaz.eedu.course.appointment.entry.assignment.AssignmentCreateModel; import de.gaz.eedu.entity.model.CreationModel; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentEntryModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentEntryModel.java index 55a6e078..35ad1ef2 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentEntryModel.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentEntryModel.java @@ -1,5 +1,6 @@ package de.gaz.eedu.course.appointment.entry.model; +import de.gaz.eedu.course.appointment.entry.assignment.AssignmentModel; import de.gaz.eedu.course.room.model.RoomModel; import de.gaz.eedu.entity.model.EntityModel; import org.jetbrains.annotations.NotNull; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentUpdateModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentUpdateModel.java index 19df8ef5..a231deb3 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentUpdateModel.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AppointmentUpdateModel.java @@ -1,5 +1,6 @@ package de.gaz.eedu.course.appointment.entry.model; +import de.gaz.eedu.course.appointment.entry.assignment.AssignmentCreateModel; import org.jetbrains.annotations.Nullable; public record AppointmentUpdateModel( diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AssignmentInsightModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AssignmentInsightModel.java deleted file mode 100644 index b9174a4a..00000000 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/model/AssignmentInsightModel.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.gaz.eedu.course.appointment.entry.model; - -import org.jetbrains.annotations.NotNull; - -public record AssignmentInsightModel(@NotNull String name, boolean submitted, @NotNull String[] files) {} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/room/RoomController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/room/RoomController.java index 4964a9e1..d8d7ce98 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/room/RoomController.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/room/RoomController.java @@ -37,6 +37,6 @@ public class RoomController extends EntityController> fetchAll() {return super.fetchAll();} } diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/course/subject/SubjectService.java b/EEDU-Backend/src/main/java/de/gaz/eedu/course/subject/SubjectService.java index 760c453f..2dd06593 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/course/subject/SubjectService.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/course/subject/SubjectService.java @@ -15,7 +15,6 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Set; import java.util.stream.Stream; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileController.java index 95d32f00..b84bfbe9 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileController.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileController.java @@ -83,4 +83,16 @@ } throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); } + + @PreAuthorize("@verificationService.isFullyAuthenticated()") @GetMapping("/get/{fileId}/{fileName}") public ResponseEntity downloadFileWithName( + @AuthenticationPrincipal Long userId, @PathVariable Long fileId, @PathVariable String fileName) throws IOException + { + Function access = file -> file.hasAccess(userService.loadEntityByIDSafe(userId)); + if (fileService.getRepository().findById(fileId).map(access).orElse(false)) + { + return fileService.loadResourceByIdAndName(fileId, fileName); + } + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + } diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileRepository.java b/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileRepository.java index ebf7d66a..73b95d7c 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileRepository.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileRepository.java @@ -3,7 +3,6 @@ import org.jetbrains.annotations.NotNull; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; import java.util.Optional; public interface FileRepository extends JpaRepository diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileService.java b/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileService.java index 65fcf4c3..110d0765 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileService.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/file/FileService.java @@ -99,6 +99,19 @@ public boolean delete(long id, @NotNull Runnable deleteTask) return zipAndSend(files); } + public @NotNull ResponseEntity loadResourceByIdAndName(@NotNull Long id, @NotNull String fileName) throws IOException + { + File directory = getDirectoryFromId(id); + File[] files = directory.listFiles((dir, name) -> name.equals(fileName)); + + if (files == null || files.length == 0) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + + return sendSingle(files[0]); + } + + public ResponseEntity zipAndSend(@NotNull File[] files) { ContentDisposition contentDisposition = ContentDisposition.builder("attachment") diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/WSInterceptor.java b/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/WSInterceptor.java index 2e86305f..4c6f06b8 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/WSInterceptor.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/WSInterceptor.java @@ -1,22 +1,16 @@ package de.gaz.eedu.livechat; import de.gaz.eedu.user.UserService; -import de.gaz.eedu.user.verification.VerificationService; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; @Slf4j @RequiredArgsConstructor diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/WebsocketController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/WebsocketController.java index cf2b8838..9d54f344 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/WebsocketController.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/WebsocketController.java @@ -1,7 +1,6 @@ package de.gaz.eedu.livechat; import com.fasterxml.jackson.core.JsonProcessingException; -import de.gaz.eedu.livechat.DTO.WebsocketChatEdit; import de.gaz.eedu.livechat.chat.ChatModel; import de.gaz.eedu.livechat.chat.ChatService; import de.gaz.eedu.livechat.message.MessageModel; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/chat/ChatEntity.java b/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/chat/ChatEntity.java index 49b31f65..691ab542 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/chat/ChatEntity.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/chat/ChatEntity.java @@ -1,9 +1,7 @@ package de.gaz.eedu.livechat.chat; import de.gaz.eedu.entity.model.EntityModelRelation; -import de.gaz.eedu.entity.model.EntityObject; import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/chat/ChatModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/chat/ChatModel.java index 3cef8af5..2274b4dd 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/chat/ChatModel.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/livechat/chat/ChatModel.java @@ -1,7 +1,6 @@ package de.gaz.eedu.livechat.chat; import de.gaz.eedu.entity.model.EntityModel; -import de.gaz.eedu.entity.model.Model; import org.jetbrains.annotations.NotNull; import java.util.Arrays; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/management/illnessnotifications/IllnessNotificationManagementController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/management/illnessnotifications/IllnessNotificationManagementController.java index 748d5fef..d0231c80 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/management/illnessnotifications/IllnessNotificationManagementController.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/management/illnessnotifications/IllnessNotificationManagementController.java @@ -30,7 +30,6 @@ public ResponseEntity getNotificationsOfDate(@NotNul return illnessNotificationManagementService.getNotificationsOfDate(date); } - @PreAuthorize("hasAuthority(T(de.gaz.eedu.user.privileges.SystemPrivileges).USER_CREATE.toString())") @GetMapping("/get-pending") public ResponseEntity> getPendingNotifications() diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/UserController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/UserController.java index 716f82d8..4e071f16 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/UserController.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/UserController.java @@ -18,7 +18,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/UserEntity.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/UserEntity.java index da73f81c..e2959dab 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/UserEntity.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/UserEntity.java @@ -247,7 +247,7 @@ public boolean attachGroups(@NotNull GroupEntity... groupEntities) throws StateT if(!Collections.disjoint(this.getGroups(), entities)) { - throw new StateTransitionException("The user already has group(s)."); + throw new StateTransitionException("The user already is part in any of the given group(s)."); } return this.groups.addAll(entities); @@ -271,7 +271,7 @@ private static boolean containsGroup(@NotNull Collection entities, * @param ids The IDs of the groups to be detached. * @return true if a group was successfully detached and the user entity was saved, false otherwise. */ - public boolean detachGroups(@NotNull UserService userService, @NotNull String... ids) + public boolean detachGroups(@NotNull UserService userService, @NotNull String... ids) throws StateTransitionException { return saveEntityIfPredicateTrue(userService, ids, this::detachGroups); } @@ -292,6 +292,13 @@ public boolean detachGroups(@NotNull UserService userService, @NotNull String... public boolean detachGroups(@NotNull String... ids) { List detachGroupIds = Arrays.asList(ids); + Set allGroups = this.groups.stream().map(GroupEntity::getId).collect(Collectors.toSet()); + + if(!allGroups.containsAll(detachGroupIds)) + { + throw new StateTransitionException("The user already is not part of some given group(s)."); + } + return this.groups.removeIf(groupEntity -> detachGroupIds.contains(groupEntity.getId())); } diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/GroupController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/GroupController.java index 81646504..d0f52391 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/GroupController.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/GroupController.java @@ -41,7 +41,7 @@ public class GroupController extends EntityController attachGroups(@PathVariable long user, @PathVariable @NotNull String... groups) { log.info("Received incoming request for attaching group(s) {} to user {}.", groups, user); - return empty(getService().attachGroups(user, groups) ? HttpStatus.OK : HttpStatus.NOT_MODIFIED); + return empty(getService().attachGroups(user, groups) ? HttpStatus.OK : HttpStatus.BAD_REQUEST); } @@ -50,7 +50,7 @@ public class GroupController extends EntityController detachGroups(@PathVariable long user, @PathVariable @NotNull String... groups) { log.info("Received incoming request for detaching group(s) {} to user {}.", groups, user); - return empty(getService().detachGroups(user, groups) ? HttpStatus.OK : HttpStatus.NOT_MODIFIED); + return empty(getService().detachGroups(user, groups) ? HttpStatus.OK : HttpStatus.BAD_REQUEST); } @PostMapping("/create") diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/GroupService.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/GroupService.java index 967737ef..d3856437 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/GroupService.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/GroupService.java @@ -88,7 +88,6 @@ private static void validateGroups(String @NotNull [] entities) throws ResponseS @Transactional public boolean attachGroups(long userId, @NotNull String[] groups) throws GroupUnprocessableException { validateGroups(groups); - GroupEntity[] entities = loadEntityById(Arrays.asList(groups)).toArray(GroupEntity[]::new); return getUserService().loadEntityByIDSafe(userId).attachGroups(getUserService(), entities); } diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/repository/GroupEntityRepository.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/repository/GroupEntityRepository.java index 9b30c662..53100c91 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/repository/GroupEntityRepository.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/group/repository/GroupEntityRepository.java @@ -2,8 +2,5 @@ import de.gaz.eedu.entity.EntityRepository; import de.gaz.eedu.user.group.GroupEntity; -import org.jetbrains.annotations.NotNull; - -import java.util.Optional; public interface GroupEntityRepository extends EntityRepository {} diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/illnessnotifications/IllnessNotificationService.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/illnessnotifications/IllnessNotificationService.java index 2ef6b204..011d72f1 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/illnessnotifications/IllnessNotificationService.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/illnessnotifications/IllnessNotificationService.java @@ -53,11 +53,11 @@ public boolean excuse(@NotNull Long userId, @NotNull String reason, @NotNull Lon LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond(), expirationTime, fileId))); - return true; } - public @Nullable Long uploadNotification(@Nullable MultipartFile file){ + public @Nullable Long uploadNotification(@Nullable MultipartFile file) throws MaliciousFileException + { if(!(file != null && !file.isEmpty())) { return null; @@ -65,18 +65,11 @@ public boolean excuse(@NotNull Long userId, @NotNull String reason, @NotNull Lon FileEntity fileEntity = fileService.createEntity(new FileCreateModel( "illness_notifications", - new String[] { "Management", "ADMINISTRATOR" }, + new String[] { "Management", "ADMINISTRATOR", "ROLE_administrator", "USER_CREATE" }, new String[] { "illness_notification" })); - try - { - fileEntity.uploadBatch("", file); - return fileEntity.getId(); - } - catch (MaliciousFileException e) - { - throw new RuntimeException(e); - } + fileEntity.uploadBatch("", file); + return fileEntity.getId(); } @Transactional diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/illnessnotifications/IllnessRequest.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/illnessnotifications/IllnessRequest.java index 9de5acfc..f281b43b 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/illnessnotifications/IllnessRequest.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/illnessnotifications/IllnessRequest.java @@ -1,7 +1,6 @@ package de.gaz.eedu.user.illnessnotifications; import org.jetbrains.annotations.NotNull; -import org.springframework.web.multipart.MultipartFile; public record IllnessRequest(@NotNull String reason, @NotNull Long expirationTime) { diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/model/ReducedUserModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/model/ReducedUserModel.java index ee364e42..f1c236f4 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/model/ReducedUserModel.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/model/ReducedUserModel.java @@ -1,14 +1,19 @@ package de.gaz.eedu.user.model; +import de.gaz.eedu.entity.model.Model; import de.gaz.eedu.user.AccountType; -import jakarta.validation.constraints.NotNull; import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.Objects; -public record ReducedUserModel(@NotNull Long id, @NotNull String firstName, @NotNull String lastName, @NotNull AccountType accountType) -{ - @Contract(pure = true) @Override public @org.jetbrains.annotations.NotNull String toString() +public record ReducedUserModel( + @NotNull Long id, + @NotNull String firstName, + @NotNull String lastName, + @NotNull AccountType accountType +) implements Model { + @Contract(pure = true) @Override public @NotNull String toString() { // Automatically generated by IntelliJ return "ReducedUserModel{" + "id=" + id + diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/privileges/SystemPrivileges.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/privileges/SystemPrivileges.java index f67d6a73..105c0b25 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/privileges/SystemPrivileges.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/privileges/SystemPrivileges.java @@ -28,6 +28,7 @@ public enum SystemPrivileges COURSE_CREATE, COURSE_ATTACH_USER, COURSE_DETACH_USER, + COURSE_ALTER_SUBJECT, COURSE_DELETE, COURSE_GET, diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeController.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeController.java index b4fe1f85..756d2fd2 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeController.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeController.java @@ -1,5 +1,6 @@ package de.gaz.eedu.user.theming; +import de.gaz.eedu.exception.EntityUnknownException; import de.gaz.eedu.exception.NameOccupiedException; import de.gaz.eedu.user.UserEntity; import de.gaz.eedu.user.UserService; @@ -66,6 +67,16 @@ public ResponseEntity setTheme(@AuthenticationPrincipal Long id, @Re return ResponseEntity.ok(fallbackTheme.toEntity(new ThemeEntity()).toModel()); } + @PreAuthorize("@verificationService.isFullyAuthenticated()") + @GetMapping("/theme/get/{themeId}") public ResponseEntity getTheme(@AuthenticationPrincipal Long id, @NotNull @PathVariable Long themeId){ + if(userService.loadEntityById(id).isPresent()) + { + return ResponseEntity.ok(themeService.getRepository().findById(themeId).orElseThrow(() -> new EntityUnknownException(themeId)).toModel()); + } + + throw new ResponseStatusException(HttpStatus.BAD_REQUEST); + } + /** * Returns all themes in the database as SimpleThemeModels. * @return SimpleThemeModel diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeCreateModel.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeCreateModel.java index 0f806e24..497dd7a7 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeCreateModel.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeCreateModel.java @@ -5,7 +5,6 @@ import org.jetbrains.annotations.NotNull; import java.util.Arrays; -import java.util.HashSet; public record ThemeCreateModel(String name, byte[] backgroundColor, byte[] widgetColor) implements CreationModel { @@ -23,7 +22,6 @@ public record ThemeCreateModel(String name, byte[] backgroundColor, byte[] widge themeEntity.setWidgetColorG(widgetColor[1]); themeEntity.setWidgetColorB(widgetColor[2]); - themeEntity.setUsers(new HashSet<>()); return themeEntity; } diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeEntity.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeEntity.java index a88350c5..6516a55f 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeEntity.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/theming/ThemeEntity.java @@ -1,8 +1,6 @@ package de.gaz.eedu.user.theming; -import com.fasterxml.jackson.annotation.JsonBackReference; import de.gaz.eedu.entity.model.EntityModelRelation; -import de.gaz.eedu.user.UserEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -10,7 +8,6 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import java.util.Set; @Setter @Getter @Entity @Table(name = "theme_entity") public class ThemeEntity implements EntityModelRelation { @@ -24,11 +21,6 @@ @Column(name = "widget_color_g") private byte widgetColorG; @Column(name = "widget_color_b") private byte widgetColorB; - @OneToMany(mappedBy = "themeEntity", cascade = { - CascadeType.REFRESH, - CascadeType.PERSIST - }) @JsonBackReference private Set users; - @Override @Contract(pure = true) public @NotNull ThemeModel toModel() { return new ThemeModel(getId(), getName(), diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/TokenData.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/TokenData.java index ae1161f5..3541f1c4 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/TokenData.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/TokenData.java @@ -2,6 +2,7 @@ import de.gaz.eedu.user.verification.authority.InvalidTokenException; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ClaimsBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.jetbrains.annotations.Contract; @@ -37,7 +38,8 @@ public TokenData(long userId, boolean advanced, @NotNull Set restrictedC userId, advanced, restrictedClaims, - Stream.of(claimHolder).collect(Collectors.toMap(ClaimHolder::key, ClaimHolder::content))); + Stream.of(claimHolder).collect(Collectors.toMap(ClaimHolder::key, ClaimHolder::content)) + ); } private static void validate(@NotNull Set keys, Set elements) throws InvalidTokenException @@ -59,7 +61,7 @@ private static void validate(@NotNull Set keys, Set elements) th // get rid of this, will be generated with next thing additionalClaims.remove("sub"); additionalClaims.remove("iat"); - additionalClaims.remove("exo"); + additionalClaims.remove("exp"); validate(additionalClaims.keySet(), restricted); @@ -68,6 +70,33 @@ private static void validate(@NotNull Set keys, Set elements) th return new TokenData(claims, userId, advanced, restricted, additionalClaims); } + @Contract("_, _ -> new") + public static @NotNull TokenData purgeClaims(@NotNull TokenData tokenData, String @NotNull ... keys) throws InvalidTokenException + { + for (String key : keys) + { + tokenData.deleteRestrictedClaim(key); + } + + return new TokenData( + tokenData.getParent().map((claims) -> removeClaims(claims, keys)).orElse(tokenData.parent()), + tokenData.userId(), + tokenData.advanced(), + tokenData.restrictedClaims(), + tokenData.additionalClaims() + ); + } + + private static Claims removeClaims(@NotNull Claims parent, @NotNull String @NotNull ... keys) + { + ClaimsBuilder claimsBuilder = Jwts.claims().add(parent); + for (String key : keys) + { + claimsBuilder.delete(key); + } + return claimsBuilder.build(); + } + @Contract("_, _ -> new") public static @NotNull TokenData deserialize(@NotNull String key, @NotNull String token) throws InvalidTokenException { @@ -101,7 +130,7 @@ public boolean addRestrictedClaim(boolean override, @NotNull String key, @NotNul public boolean deleteRestrictedClaim(@NotNull String key) { - return restrictedClaims().remove(key) && removeClaim(key); + return restrictedClaims().remove(key) || removeClaim(key); } public boolean unrestrictedClaim(@NotNull String key) diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/VerificationService.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/VerificationService.java index f4c088f2..52e979f5 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/VerificationService.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/VerificationService.java @@ -212,11 +212,7 @@ public boolean hasToken(@NotNull Authentication authentication, @NotNull JwtToke JwtTokenType type = tokenData.advanced() ? JwtTokenType.ADVANCED_AUTHORIZATION : JwtTokenType.AUTHORIZED; Instant expiry = Instant.ofEpochMilli(tokenData.get("expiry", Long.class)); - tokenData.deleteRestrictedClaim("expiry"); - tokenData.deleteRestrictedClaim("available"); - tokenData.deleteRestrictedClaim("temporary"); - - return generateKey(type, expiry, tokenData); + return generateKey(type, expiry, TokenData.purgeClaims(tokenData, "expiry", "available", "temporary")); } /** diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/credentials/implementations/CredentialMethod.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/credentials/implementations/CredentialMethod.java index 1f2646bc..799b5923 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/credentials/implementations/CredentialMethod.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/credentials/implementations/CredentialMethod.java @@ -15,7 +15,7 @@ public enum CredentialMethod PASSWORD(new PasswordCredential(new BCryptPasswordEncoder()), false), EMAIL(new EmailCredential(), true), SMS(new SMSCredential(), true), - TOTP(new TOTPCredential(new TOPTHandler(HashingAlgorithm.SHA1)), true); + TOTP(new TOTPCredential(new TOPTHandler(HashingAlgorithm.SHA256)), true); private final Credential credential; private final boolean enablingRequired; diff --git a/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/credentials/implementations/TOTPCredential.java b/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/credentials/implementations/TOTPCredential.java index 98b7258b..40c920be 100644 --- a/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/credentials/implementations/TOTPCredential.java +++ b/EEDU-Backend/src/main/java/de/gaz/eedu/user/verification/credentials/implementations/TOTPCredential.java @@ -32,7 +32,7 @@ public class TOTPCredential implements Credential { UserEntity userEntity = credentialEntity.getUser(); String secret = credentialEntity.getSecret(); - return new TOTPData(userEntity.getLoginName(), secret, HashingAlgorithm.SHA1.getFriendlyName(), 6, 30); + return new TOTPData(userEntity.getLoginName(), secret, HashingAlgorithm.SHA256.getFriendlyName(), 6, 30); } @Override public boolean verify(@NotNull CredentialEntity credentialEntity, @NotNull String code) diff --git a/EEDU-Backend/src/main/resources/schema.sql b/EEDU-Backend/src/main/resources/schema.sql index cf543c7a..e643a2f2 100644 --- a/EEDU-Backend/src/main/resources/schema.sql +++ b/EEDU-Backend/src/main/resources/schema.sql @@ -108,6 +108,16 @@ CREATE TABLE IF NOT EXISTS user_entity FOREIGN KEY (class_room_id) REFERENCES class_room_entity (id) ); +CREATE TABLE IF NOT EXISTS assessment_entity +( + id BIGINT PRIMARY KEY NOT NULL, + appointment_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + feedback VARCHAR(200) NULL, + FOREIGN KEY (appointment_id) REFERENCES appointment_entry_entity (id), + FOREIGN KEY (user_id) REFERENCES user_entity (id) +); + CREATE TABLE IF NOT EXISTS credential_entity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, diff --git a/EEDU-Backend/src/test/java/de/gaz/eedu/user/UserServiceTest.java b/EEDU-Backend/src/test/java/de/gaz/eedu/user/UserServiceTest.java index fb65272d..4c0279b0 100644 --- a/EEDU-Backend/src/test/java/de/gaz/eedu/user/UserServiceTest.java +++ b/EEDU-Backend/src/test/java/de/gaz/eedu/user/UserServiceTest.java @@ -97,8 +97,8 @@ public class UserServiceTest extends ServiceTest test(Eval.eval(groupEntity, true, Validator.equals()), userEntity::attachGroups); if(userID == 1) @@ -133,7 +133,12 @@ public void testAttachGroup(long userID) { @ValueSource(longs = {1, 2}) @Transactional(Transactional.TxType.REQUIRES_NEW) public void testDetachGroup(long userID) { - UserEntity userEntity = getService().loadEntityById(userID).orElseThrow(IllegalStateException::new); - test(Eval.eval("group0", userID == 1, Validator.equals()), userEntity::detachGroups); + UserEntity userEntity = getService().loadEntityById(userID).orElseThrow(); + if(userID == 1) + { + test(Eval.eval("group0", true, Validator.equals()), userEntity::detachGroups); + return; + } + Assertions.assertThrowsExactly(StateTransitionException.class, () -> userEntity.detachGroups("group0")); } } diff --git a/EEDU-Frontend/src/app/abstract/abstract.component.ts b/EEDU-Frontend/src/app/abstract/abstract.component.ts index 562e8b12..c256cc9a 100644 --- a/EEDU-Frontend/src/app/abstract/abstract.component.ts +++ b/EEDU-Frontend/src/app/abstract/abstract.component.ts @@ -29,7 +29,6 @@ import {MatButton, MatIconButton} from "@angular/material/button"; export class AbstractComponent implements OnInit { private _mobile: boolean = false; - private _portrait: boolean = false; constructor(public websocketService: WebsocketService, public router: Router, public userService: UserService) { } @@ -67,27 +66,16 @@ export class AbstractComponent implements OnInit { this._mobile = window.innerWidth <= 600; } - private isPortrait() - { - this._portrait = window.innerHeight > window.innerWidth; - } - get mobile(): boolean { return this._mobile; } - get portrait(): boolean { - return this._portrait; - } - @HostListener("window:resize") public onResize() { this.isMobile(); - this.isPortrait(); } ngOnInit(): void { this.isMobile(); - this.isPortrait(); } } diff --git a/EEDU-Frontend/src/app/chat/chat-creation/chat-creation.component.scss b/EEDU-Frontend/src/app/chat/chat-creation/chat-creation.component.scss index a310ead4..67adc40a 100644 --- a/EEDU-Frontend/src/app/chat/chat-creation/chat-creation.component.scss +++ b/EEDU-Frontend/src/app/chat/chat-creation/chat-creation.component.scss @@ -1,5 +1,5 @@ .dialog-container { - background-color: white; + background-color: var(--widget-color); font-family: 'Arial', sans-serif; text-align: center; padding: 20px; @@ -9,6 +9,8 @@ flex-direction: column; align-items: center; overflow: hidden; + color: var(--text-color); +} .user-list { width: 100%; @@ -28,13 +30,13 @@ padding: 12px 16px; text-align: left; font-size: 14px; - background-color: #ffffff; + background-color: var(--widget-color); border-radius: 6px; - color: #333; + color: var(--text-color); transition: all 0.3s; box-sizing: border-box; } .mat-divider { margin: 0; -}} +} diff --git a/EEDU-Frontend/src/app/chat/chat-creation/chat-creation.component.ts b/EEDU-Frontend/src/app/chat/chat-creation/chat-creation.component.ts index a0b3d140..d8911203 100644 --- a/EEDU-Frontend/src/app/chat/chat-creation/chat-creation.component.ts +++ b/EEDU-Frontend/src/app/chat/chat-creation/chat-creation.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit} from '@angular/core'; +import {Component} from '@angular/core'; import {ReducedUserModel} from "../../user/reduced-user-model"; import {HttpClient} from "@angular/common/http"; import {MatListSubheaderCssMatStyler} from "@angular/material/list"; @@ -11,6 +11,7 @@ import {NgForOf} from "@angular/common"; import {FormsModule} from "@angular/forms"; import {UserService} from "../../user/user.service"; import {ChatModel} from "../models/chat-model"; +import {environment} from "../../../environment/environment"; @Component({ selector: 'app-chat-creation', @@ -39,7 +40,7 @@ export class ChatCreationComponent { } private getAllUsers() { - this.http.get("http://localhost:8080/api/v1/user/all/reduced", { withCredentials: true }) + this.http.get(`${environment.backendUrl}/user/all/reduced`, { withCredentials: true }) .subscribe(list => { this.originalUserList = [...list]; this.userList = list; @@ -61,7 +62,7 @@ export class ChatCreationComponent { public createChat(userId: bigint) { let chatUsers = [this.userService.getUserData.id, userId] - return this.http.post("http://localhost:8080/api/v1/chat/create", chatUsers, { + return this.http.post(`${environment.backendUrl}/chat/create`, chatUsers, { withCredentials: true }).subscribe(model => { console.log(model); diff --git a/EEDU-Frontend/src/app/chat/chat.component.html b/EEDU-Frontend/src/app/chat/chat.component.html index 94caaed6..1ded0887 100644 --- a/EEDU-Frontend/src/app/chat/chat.component.html +++ b/EEDU-Frontend/src/app/chat/chat.component.html @@ -1,7 +1,7 @@

-

Open new chat

+

Open new chat

menu
diff --git a/EEDU-Frontend/src/app/chat/chat.component.ts b/EEDU-Frontend/src/app/chat/chat.component.ts index 218fd6b9..d575dae1 100644 --- a/EEDU-Frontend/src/app/chat/chat.component.ts +++ b/EEDU-Frontend/src/app/chat/chat.component.ts @@ -18,6 +18,7 @@ import { MatDrawer, MatDrawerContainer, } from "@angular/material/sidenav"; +import {ChatService} from "./chat.service"; @Component({ selector: 'app-chat', @@ -50,7 +51,7 @@ export class ChatComponent implements OnInit, AfterViewChecked { notificationList: bigint[] = []; messageContent!: string; - constructor(public dialog: Dialog, public websocketService: WebsocketService, public http: HttpClient, public userService: UserService, private cdr: ChangeDetectorRef) { + constructor(public dialog: Dialog, public chatService: ChatService, public websocketService: WebsocketService, public http: HttpClient, public userService: UserService, private cdr: ChangeDetectorRef) { } public ngOnInit() { @@ -67,9 +68,7 @@ export class ChatComponent implements OnInit, AfterViewChecked { } public getAllChats() { - return this.http.get("http://localhost:8080/api/v1/chat/getChatList", { - withCredentials: true - }).subscribe(models => { + this.chatService.getAllChats().subscribe((models: ChatModel[]): void => { this.chatList = models; console.log(models); }); @@ -99,9 +98,7 @@ export class ChatComponent implements OnInit, AfterViewChecked { } public getChat(chatId: number) { - this.http.post("http://localhost:8080/api/v1/chat/get/chat", chatId, { - withCredentials: true - }).subscribe(model => { + this.chatService.getChat(chatId).subscribe(model => { this.currentChatHistory = model; console.log(this.currentChatHistory); }); diff --git a/EEDU-Frontend/src/app/chat/chat.service.spec.ts b/EEDU-Frontend/src/app/chat/chat.service.spec.ts new file mode 100644 index 00000000..4d8abdfc --- /dev/null +++ b/EEDU-Frontend/src/app/chat/chat.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ChatService } from './chat.service'; + +describe('ChatService', () => { + let service: ChatService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ChatService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/EEDU-Frontend/src/app/chat/chat.service.ts b/EEDU-Frontend/src/app/chat/chat.service.ts new file mode 100644 index 00000000..c85da7f3 --- /dev/null +++ b/EEDU-Frontend/src/app/chat/chat.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import {HttpClient} from "@angular/common/http"; +import {ChatModel} from "./models/chat-model"; +import {environment} from "../../environment/environment"; +import {Observable} from "rxjs"; +import {MessageModel} from "./models/message-model"; + +@Injectable({ + providedIn: 'root' +}) +export class ChatService { + constructor(public http: HttpClient) { } + + public getAllChats(): Observable { + return this.http.get(`${environment.backendUrl}/chat/getChatList`, { + withCredentials: true + }); + } + + public getChat(chatId: number): Observable { + return this.http.post(`${environment.backendUrl}/chat/get/chat`, chatId, { + withCredentials: true + }); + } +} diff --git a/EEDU-Frontend/src/app/chat/websocket.service.ts b/EEDU-Frontend/src/app/chat/websocket.service.ts index bbb8242a..99fc2ad2 100644 --- a/EEDU-Frontend/src/app/chat/websocket.service.ts +++ b/EEDU-Frontend/src/app/chat/websocket.service.ts @@ -1,9 +1,10 @@ -import {Injectable, OnInit} from '@angular/core'; +import {Injectable} from '@angular/core'; import {Stomp} from "@stomp/stompjs"; import {AuthenticationService} from "../user/authentication/authentication.service"; import {UserService} from "../user/user.service"; import {HttpClient} from "@angular/common/http"; -import {map, merge, Observable} from "rxjs"; +import {Observable} from "rxjs"; +import {environment} from "../../environment/environment"; @Injectable({ providedIn: 'root' @@ -20,7 +21,7 @@ export class WebsocketService { private connect(onConnectCallback: () => void) { this.authenticateWebsocket().subscribe(token => { - const socket = new WebSocket(`ws://localhost:8080/ws-endpoint?token=${token}`); + const socket = new WebSocket(`${environment.websocketUrl}?token=${token}`); this.stompClient = Stomp.over(socket); this.stompClient.connect({}, (frame: any): void => { this.listen('test'); @@ -51,7 +52,7 @@ export class WebsocketService { private authenticateWebsocket(): Observable { - return this.http.get('http://localhost:8080/api/v1/chat/authenticate', { + return this.http.get(`${environment.backendUrl}/chat/authenticate`, { withCredentials: true, responseType: "text" as "json" }); diff --git a/EEDU-Frontend/src/app/common/abstract-list/abstract-list.component.ts b/EEDU-Frontend/src/app/common/abstract-list/abstract-list.component.ts index 89a9a67f..39c6f90b 100644 --- a/EEDU-Frontend/src/app/common/abstract-list/abstract-list.component.ts +++ b/EEDU-Frontend/src/app/common/abstract-list/abstract-list.component.ts @@ -37,6 +37,7 @@ export interface GeneralListInfo { selector: 'list', imports: [MatChipSet, MatChip, MatExpansionPanel, MatAccordion, MatExpansionPanelTitle, MatExpansionPanelDescription, MatExpansionPanelHeader, MatFormField, MatInput, MatLabel, NgIf, FormsModule, NgForOf, AllCheckBoxComponent, SingleCheckBoxComponent, NgComponentOutlet, MatList, MatListItem, NgTemplateOutlet,], templateUrl: './abstract-list.component.html', + standalone: true, styleUrl: './abstract-list.component.scss' }) export class AbstractList { diff --git a/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.html b/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.html index 5ab98241..b6a8c9c4 100644 --- a/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.html +++ b/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.html @@ -1,6 +1,6 @@
- warning + {{ icon() }}

{{ message() }}

diff --git a/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.scss b/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.scss index 30ce2f8f..a1fc2c61 100644 --- a/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.scss +++ b/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.scss @@ -8,6 +8,11 @@ .error-icon { margin-bottom: 16px; + + .actual-icon + { + font-size: 64px; height: 64px; width: 64px; + } } .error-message p strong { diff --git a/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.ts b/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.ts index 7cd5255c..3ea7ce16 100644 --- a/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.ts +++ b/EEDU-Frontend/src/app/common/general-error-box/general-error-box.component.ts @@ -9,6 +9,7 @@ import {NgIf} from "@angular/common"; styleUrl: './general-error-box.component.scss' }) export class GeneralErrorBoxComponent { + public readonly icon: InputSignal = input('warning'); public readonly message: InputSignal = input(''); public readonly subMessage: InputSignal = input(null); } diff --git a/EEDU-Frontend/src/app/common/selection-input/selection-input.component.html b/EEDU-Frontend/src/app/common/selection-input/selection-input.component.html index 20911237..c3cb3970 100644 --- a/EEDU-Frontend/src/app/common/selection-input/selection-input.component.html +++ b/EEDU-Frontend/src/app/common/selection-input/selection-input.component.html @@ -35,7 +35,6 @@ [(ngModel)]="currentValue" [placeholder]="placeholder()" [matAutocomplete]="complete" - [disabled]="accessibleValues().length == 1" (blur)="onTouched()" /> } diff --git a/EEDU-Frontend/src/app/common/selection-input/selection-input.component.ts b/EEDU-Frontend/src/app/common/selection-input/selection-input.component.ts index 84794081..7c0f5439 100644 --- a/EEDU-Frontend/src/app/common/selection-input/selection-input.component.ts +++ b/EEDU-Frontend/src/app/common/selection-input/selection-input.component.ts @@ -185,6 +185,11 @@ export class SelectionInput implement return; } + if(Array.isArray(value)) { + this.selectedValues.set(value as T[]); + return; + } + this.currentValue.set(this.toName(value)); } @@ -254,11 +259,6 @@ export class SelectionInput implement protected add(event: MatChipInputEvent): string { const value: T[] = this.filter((event.value || '').trim()); - if(value.length != 1) - { - return event.value; - } - this.value = value[0]; return ''; } diff --git a/EEDU-Frontend/src/app/dashboard/appointment-card/appointment-card.component.html b/EEDU-Frontend/src/app/dashboard/appointment-card/appointment-card.component.html index e040c587..f2864c72 100644 --- a/EEDU-Frontend/src/app/dashboard/appointment-card/appointment-card.component.html +++ b/EEDU-Frontend/src/app/dashboard/appointment-card/appointment-card.component.html @@ -1 +1,12 @@ -

appointment-card works!

+
+
+
+

{{ x.description }}

+ +
+
+
+ +
+ No future assignments! +
diff --git a/EEDU-Frontend/src/app/dashboard/appointment-card/appointment-card.component.ts b/EEDU-Frontend/src/app/dashboard/appointment-card/appointment-card.component.ts index 92796f40..b706f920 100644 --- a/EEDU-Frontend/src/app/dashboard/appointment-card/appointment-card.component.ts +++ b/EEDU-Frontend/src/app/dashboard/appointment-card/appointment-card.component.ts @@ -1,19 +1,26 @@ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; import {AppointmentService} from "../../user/courses/appointment/appointment.service"; import {AppointmentEntryModel} from "../../user/courses/appointment/entry/appointment-entry-model"; +import {NgForOf, NgIf} from "@angular/common"; @Component({ - selector: 'app-appointment-card', - standalone: true, - imports: [], - templateUrl: './appointment-card.component.html', - styleUrl: './appointment-card.component.scss' + selector: 'app-appointment-card', + standalone: true, + imports: [NgForOf, NgIf], + templateUrl: './appointment-card.component.html', + styleUrl: './appointment-card.component.scss' }) export class AppointmentCardComponent { - public constructor(private readonly _appointmentService: AppointmentService) {} + public constructor(appointmentService: AppointmentService) { + appointmentService.nextAppointments.subscribe((appointments: readonly AppointmentEntryModel[]): void => { + this._appointments = appointments; + }); + } + + private _appointments: readonly AppointmentEntryModel[] = []; protected get appointments(): readonly AppointmentEntryModel[] { - return this._appointmentService.nextAppointments.slice(0, 5); + return this._appointments; } } diff --git a/EEDU-Frontend/src/app/dashboard/assignment-card/assignment-card.component.ts b/EEDU-Frontend/src/app/dashboard/assignment-card/assignment-card.component.ts index 12a63743..213d769d 100644 --- a/EEDU-Frontend/src/app/dashboard/assignment-card/assignment-card.component.ts +++ b/EEDU-Frontend/src/app/dashboard/assignment-card/assignment-card.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; -import {AssignmentModel} from "../../user/courses/appointment/entry/assignment-model"; -import {AppointmentService} from "../../user/courses/appointment/appointment.service"; +import {AssignmentService} from "../../user/courses/appointment/entry/assignment/assignment.service"; +import {AssignmentModel} from "../../user/courses/appointment/entry/assignment/assignment-model"; @Component({ selector: 'app-assignment-card', @@ -11,9 +11,17 @@ import {AppointmentService} from "../../user/courses/appointment/appointment.ser }) export class AssignmentCardComponent { - public constructor(private readonly _appointmentService: AppointmentService) {} + private _assignments: readonly AssignmentModel[] = []; + + public constructor(assignmentService: AssignmentService) + { + assignmentService.nextAssignments.subscribe((assignment: readonly AssignmentModel[]): void => + { + this._assignments = assignment; + }) + } protected get assignments(): readonly AssignmentModel[] { - return this._appointmentService.nextAssignments.slice(0, 5); + return this._assignments.slice(0, 5); } } diff --git a/EEDU-Frontend/src/app/dashboard/dashboard.component.html b/EEDU-Frontend/src/app/dashboard/dashboard.component.html index cc0bc85b..f147db74 100644 --- a/EEDU-Frontend/src/app/dashboard/dashboard.component.html +++ b/EEDU-Frontend/src/app/dashboard/dashboard.component.html @@ -9,8 +9,10 @@

My Dashboard

-
-

{{ card.title }}

-

+
+
+

{{ card.title }}

+ +
diff --git a/EEDU-Frontend/src/app/dashboard/dashboard.component.scss b/EEDU-Frontend/src/app/dashboard/dashboard.component.scss index 9a4cba79..12a5fbc3 100644 --- a/EEDU-Frontend/src/app/dashboard/dashboard.component.scss +++ b/EEDU-Frontend/src/app/dashboard/dashboard.component.scss @@ -62,15 +62,17 @@ body { } .card { + width: 100%; color: var(--text-color); background: var(--widget-color); border-radius: 8px; padding: 20px; - text-align: center; + text-align: left; transition: transform 0.3s ease, box-shadow 0.3s ease; display: flex; flex-direction: column; align-items: center; + cursor: pointer; } .card:hover { diff --git a/EEDU-Frontend/src/app/dashboard/dashboard.component.ts b/EEDU-Frontend/src/app/dashboard/dashboard.component.ts index 25df273c..28e347ee 100644 --- a/EEDU-Frontend/src/app/dashboard/dashboard.component.ts +++ b/EEDU-Frontend/src/app/dashboard/dashboard.component.ts @@ -1,5 +1,4 @@ import { Component } from '@angular/core'; -import {ThemeService} from "../theming/theme.service"; import {UserService} from "../user/user.service"; import {AppointmentCardComponent} from "./appointment-card/appointment-card.component"; import {AssignmentCardComponent} from "./assignment-card/assignment-card.component"; @@ -7,6 +6,7 @@ import {NewsCardComponent} from "./news-card/news-card.component"; import {ChatCardComponent} from "./chat-card/chat-card.component"; import {MatIcon} from "@angular/material/icon"; import {NgComponentOutlet, NgForOf} from "@angular/common"; +import {Router, RouterLink} from "@angular/router"; @Component({ selector: 'app-dashboard', @@ -14,23 +14,28 @@ import {NgComponentOutlet, NgForOf} from "@angular/common"; imports: [ MatIcon, NgForOf, - NgComponentOutlet + NgComponentOutlet, + RouterLink ], standalone: true, styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent { - constructor(public themeService: ThemeService, public userService: UserService) { + constructor(public userService: UserService, public router: Router) { } cards = [ - { title: 'Next appointments', content: 'This is the content of card 1.', component: AppointmentCardComponent }, - { title: 'Homework', content: 'This is the content of card 2.', component: AssignmentCardComponent }, - { title: 'Latest news', content: 'This is the content of card 3.', component: NewsCardComponent }, - { title: 'Latest contacts', content: 'This is the content of card 4.', component: ChatCardComponent } + { title: 'Next appointments', component: AppointmentCardComponent, route: 'timetable' }, + { title: 'Homework', component: AssignmentCardComponent, route: 'timetable'}, + { title: 'Latest news', component: NewsCardComponent, route: 'news' }, + { title: 'Latest contacts', component: ChatCardComponent, route: 'chat' } ]; public get user() { return this.userService.getUserData; } + + public navigateTo(route: string): void { + this.router.navigate([route]); + } } diff --git a/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.html b/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.html index 0e958cef..fd51aa89 100644 --- a/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.html +++ b/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.html @@ -1 +1,12 @@ -

news-card works!

+
+
+
+

{{ x.title }}

+ +
+
+
+ +
+ No articles yet! +
diff --git a/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.scss b/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.scss index e69de29b..cf2fa830 100644 --- a/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.scss +++ b/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.scss @@ -0,0 +1,29 @@ +.post-item { + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + transition: background 0.3s ease-in-out; + border-radius: 6px; + + &:last-child { + border-bottom: none; + } +} + +.post-header { + display: flex; + justify-content: space-between; +} + +.post-title { + font-size: 16px; + font-weight: 600; + margin: 0; + color: var(--text-color); +} + +.post-author { + font-size: 13px; + color: var(--text-color); + margin: 0; + font-style: italic; +} diff --git a/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.ts b/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.ts index 3454616d..6e0d7f23 100644 --- a/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.ts +++ b/EEDU-Frontend/src/app/dashboard/news-card/news-card.component.ts @@ -1,12 +1,35 @@ -import { Component } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; +import {NewsService} from "../../news/news.service"; +import {PostModel} from "../../news/post-model"; +import {NgForOf, NgIf} from "@angular/common"; +import {MatCard, MatCardContent, MatCardHeader, MatCardSubtitle, MatCardTitle} from "@angular/material/card"; @Component({ - selector: 'app-news-card', - standalone: true, - imports: [], - templateUrl: './news-card.component.html', - styleUrl: './news-card.component.scss' + selector: 'app-news-card', + standalone: true, + imports: [ + NgForOf, + NgIf, + MatCard, + MatCardHeader, + MatCardContent, + MatCardTitle, + MatCardSubtitle + ], + templateUrl: './news-card.component.html', + styleUrl: './news-card.component.scss' }) -export class NewsCardComponent { +export class NewsCardComponent implements OnInit { + postList: PostModel[] = []; + + constructor(public newsService: NewsService) { + } + + ngOnInit(): void { + this.newsService.getPosts().subscribe(posts => { + this.postList = posts.splice(0, 6); + console.log(this.postList); + }); + } } diff --git a/EEDU-Frontend/src/app/entity/entity-list/entity-list.component.html b/EEDU-Frontend/src/app/entity/entity-list/entity-list.component.html index 385936a0..3af8f5b4 100644 --- a/EEDU-Frontend/src/app/entity/entity-list/entity-list.component.html +++ b/EEDU-Frontend/src/app/entity/entity-list/entity-list.component.html @@ -5,7 +5,7 @@ @if (hasPrivilege(service()!.privileges.fetchPrivilege)) {
folder_open - +
{ private readonly _http: HttpClient, private readonly _location: string, private readonly _defaultRequiredPrivileges: DefaultRequiredPrivileges, - private readonly _createDialog: ComponentType) {} + private readonly _createDialog: ComponentType + ) {} private _fetched: boolean = false @@ -41,14 +42,14 @@ export abstract class EntityService { } public get value(): T[] { - return this.value$.value; + return this._subject.value; } - public get value$(): BehaviorSubject { + public get value$(): Observable { if (!this.fetched) { this.fetchAll.subscribe(); } - return this._subject; + return this._subject.asObservable(); } public get fetchAll(): Observable { @@ -100,7 +101,7 @@ export abstract class EntityService { } public update(): void { - this.value$.next([...this.value]); + this._subject.next([...this.value]); } protected toPackets(models: C[]): any[] { @@ -113,11 +114,11 @@ export abstract class EntityService { }); } - protected pushCreated(response: T[]): void { + protected pushCreated(response: readonly T[]): void { this._subject.next([...this.value, ...response]); } protected postDelete(id: P[]): void { - this.value$.next(this.value.filter(((value: T): boolean => !id.includes(value.id)))); + this._subject.next(this.value.filter(((value: T): boolean => !id.includes(value.id)))); } } diff --git a/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.html b/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.html index d8e95a69..dc8674ed 100644 --- a/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.html +++ b/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.html @@ -1,7 +1,7 @@

Select image

- +
  • {{ file.name }}
diff --git a/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.scss b/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.scss index f200152b..f50d0fdf 100644 --- a/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.scss +++ b/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.scss @@ -1,9 +1,9 @@ .upload-container { text-align: center; padding: 20px; - border: 1px solid #cccccc; + border: 1px dashed var(--text-color); border-radius: 8px; - background-color: #f8f8f8; + background-color: var(--background-color); max-width: 400px; margin-top: 10px; margin-bottom: 10px; @@ -11,14 +11,14 @@ .upload-container p { font-size: 16px; - color: #333; + color: var(--text-color); } -button { +.button { padding: 10px 20px; font-size: 14px; - background-color: #00796b; - color: white; + background-color: var(--widget-color); + color: var(--text-color); border: none; border-radius: 5px; cursor: pointer; diff --git a/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.ts b/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.ts index 24226fc4..71ccdd21 100644 --- a/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.ts +++ b/EEDU-Frontend/src/app/file/file-upload-button/file-upload-button.component.ts @@ -1,7 +1,6 @@ import {Component, EventEmitter, Output} from '@angular/core'; import {NgForOf} from "@angular/common"; import {MatButton} from "@angular/material/button"; -import {ArticleCreationComponent} from "../../news/article-creation/article-creation.component"; import {ArticleCreationService} from "../../news/article-creation/article-creation.service"; @Component({ diff --git a/EEDU-Frontend/src/app/file/file.service.ts b/EEDU-Frontend/src/app/file/file.service.ts index 11c86f9e..fa5802ec 100644 --- a/EEDU-Frontend/src/app/file/file.service.ts +++ b/EEDU-Frontend/src/app/file/file.service.ts @@ -2,6 +2,7 @@ import {HttpClient, HttpEvent} from "@angular/common/http"; import {FileModel} from "./file-model"; import {Observable} from "rxjs"; import {Injectable} from '@angular/core'; +import {environment} from "../../environment/environment"; interface FileResponse { blob: Uint8Array; @@ -16,21 +17,12 @@ interface FileResponse { * https://blog.angular-university.io/angular-file-upload/ */ export class FileService { - URL_PREFIX: string = "http://localhost:8080/api/v1/file"; + URL_PREFIX: string = `${environment.backendUrl}/file`; public selectedFiles!: File[] | null; constructor(private http: HttpClient) { } - public uploadImageToPost() - { - this.uploadSelection("http://localhost:8080/api/v1/blog/post"); - } - - public testDownload(): void { - this.downloadFile(BigInt(1)); - } - // ------------------------------ UPLOAD ----------------------------------- public uploadSelection(url: string, additionalData?: { [key: string]: any }): void { if(this.selectedFiles){ @@ -89,9 +81,9 @@ export class FileService { } // ------------------------------ DOWNLOAD ----------------------------------- - public async fetchFile(id: bigint, index?: number): Promise { + public async fetchFile(id: bigint, identifier?: number | string): Promise { console.log("Fetching file binaries..."); - const url: string = index == null ? `${(this.URL_PREFIX)}/get/${id}` : `${(this.URL_PREFIX)}/get/${id}/${index}` + const url: string = identifier == null ? `${(this.URL_PREFIX)}/get/${id}` : `${(this.URL_PREFIX)}/get/${id}/${identifier}` const response: Response = await fetch(url, { method: 'GET', headers: { diff --git a/EEDU-Frontend/src/app/illness-notification/illness-notification.component.ts b/EEDU-Frontend/src/app/illness-notification/illness-notification.component.ts index f30b600d..7a0f577d 100644 --- a/EEDU-Frontend/src/app/illness-notification/illness-notification.component.ts +++ b/EEDU-Frontend/src/app/illness-notification/illness-notification.component.ts @@ -13,9 +13,9 @@ import {MatIcon} from "@angular/material/icon"; import {NgForOf, NgIf} from "@angular/common"; import {HttpClient} from "@angular/common/http"; import {ReducedIllnessNotificationModel} from "./model/reduced-illness-notification-model"; -import {Observable, Subscription} from "rxjs"; import {IllnessNotificationStatus} from "./illness-notification-status"; import {FileUploadComponent} from "../common/file-upload/file-upload.component"; +import {IllnessNotificationService} from "./illness-notification.service"; @Component({ selector: 'app-illness-notification', @@ -43,27 +43,20 @@ import {FileUploadComponent} from "../common/file-upload/file-upload.component"; export class IllnessNotificationComponent implements OnInit { selectedFile: File | null = null; - prefix: string = "http://localhost:8080/api/v1/illness/me"; reason!: string; until!: Date | null; illnessNotifications!: ReducedIllnessNotificationModel[]; - constructor(private http: HttpClient) {} + constructor(private http: HttpClient, private service: IllnessNotificationService) {} ngOnInit(): void { - this.getOwnSickNotes().subscribe(list => { + this.service.getOwnSickNotes().subscribe(list => { this.illnessNotifications = list.sort(model => Number(model.id)); console.log(this.illnessNotifications); }) } - getOwnSickNotes(): Observable { - return this.http.get(`${this.prefix}/my-notifications`, { - withCredentials: true - }); - } - onFilesSelected(files: FileList): void { this.selectedFile = files.item(0); } @@ -92,7 +85,7 @@ export class IllnessNotificationComponent implements OnInit { this.until = event.value; } - onRequestSent(): Subscription | undefined { + onRequestSent(): void { console.log(this.until); if (!this.until) { @@ -114,8 +107,6 @@ export class IllnessNotificationComponent implements OnInit { formData.append("file", this.selectedFile); - return this.http.post(`${this.prefix}/excuse`, formData, { - withCredentials: true - }).subscribe(() => location.reload()); + this.service.sendSickNoteRequest(formData); } } diff --git a/EEDU-Frontend/src/app/illness-notification/illness-notification.service.spec.ts b/EEDU-Frontend/src/app/illness-notification/illness-notification.service.spec.ts new file mode 100644 index 00000000..7f12f26f --- /dev/null +++ b/EEDU-Frontend/src/app/illness-notification/illness-notification.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { IllnessNotificationService } from './illness-notification.service'; + +describe('IllnessNotificationService', () => { + let service: IllnessNotificationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(IllnessNotificationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/EEDU-Frontend/src/app/illness-notification/illness-notification.service.ts b/EEDU-Frontend/src/app/illness-notification/illness-notification.service.ts new file mode 100644 index 00000000..108caf9b --- /dev/null +++ b/EEDU-Frontend/src/app/illness-notification/illness-notification.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import {ReducedIllnessNotificationModel} from "./model/reduced-illness-notification-model"; +import {HttpClient} from "@angular/common/http"; +import {environment} from "../../environment/environment"; +import {Observable, Subscription} from "rxjs"; + +@Injectable({ + providedIn: 'root' +}) +export class IllnessNotificationService { + protected backendUrl = environment.backendUrl; + protected prefix: string = `${this.backendUrl}/illness/me`; + + constructor(public http: HttpClient) { } + + getOwnSickNotes(): Observable { + return this.http.get(`${this.prefix}/my-notifications`, { + withCredentials: true + }); + } + + sendSickNoteRequest(formData: FormData): Subscription + { + return this.http.post(`${this.prefix}/excuse`, formData, { + withCredentials: true + }).subscribe(() => location.reload()); + } +} diff --git a/EEDU-Frontend/src/app/news/article-creation/article-creation.component.html b/EEDU-Frontend/src/app/news/article-creation/article-creation.component.html index 66575312..d1c92a6f 100644 --- a/EEDU-Frontend/src/app/news/article-creation/article-creation.component.html +++ b/EEDU-Frontend/src/app/news/article-creation/article-creation.component.html @@ -10,7 +10,7 @@

Create new article

Title - + Specify an author @@ -18,7 +18,7 @@

Create new article

Author - +
Write the article body @@ -27,12 +27,12 @@

Create new article

Hint: ElementEDU uses markdown to style your articles. (max. 65000 characters) - +
Upload article thumbnail - + diff --git a/EEDU-Frontend/src/app/news/article-creation/article-creation.component.scss b/EEDU-Frontend/src/app/news/article-creation/article-creation.component.scss index 679c5ab7..b629cfbf 100644 --- a/EEDU-Frontend/src/app/news/article-creation/article-creation.component.scss +++ b/EEDU-Frontend/src/app/news/article-creation/article-creation.component.scss @@ -8,6 +8,10 @@ margin-bottom: -40px; } +.step-buttons { + background-color: var(--widget-color); +} + .stepper { overflow: auto; diff --git a/EEDU-Frontend/src/app/news/article-creation/article-creation.component.ts b/EEDU-Frontend/src/app/news/article-creation/article-creation.component.ts index 0667cb04..17ecbf65 100644 --- a/EEDU-Frontend/src/app/news/article-creation/article-creation.component.ts +++ b/EEDU-Frontend/src/app/news/article-creation/article-creation.component.ts @@ -12,6 +12,7 @@ import {MatIcon} from "@angular/material/icon"; import {NgForOf} from "@angular/common"; import {HttpClient} from "@angular/common/http"; import {ArticleCreationService} from "./article-creation.service"; +import {FileUploadComponent} from "../../common/file-upload/file-upload.component"; @Injectable({ providedIn: 'root' @@ -36,7 +37,8 @@ import {ArticleCreationService} from "./article-creation.service"; MatChipGrid, MatChipInput, NgForOf, - MatStepperNext + MatStepperNext, + FileUploadComponent ], templateUrl: './article-creation.component.html', styleUrl: './article-creation.component.scss' diff --git a/EEDU-Frontend/src/app/news/article-creation/article-creation.service.ts b/EEDU-Frontend/src/app/news/article-creation/article-creation.service.ts index 06038518..0dbabf69 100644 --- a/EEDU-Frontend/src/app/news/article-creation/article-creation.service.ts +++ b/EEDU-Frontend/src/app/news/article-creation/article-creation.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import {PostCreateModel} from "./post-create-model"; import {PostModel} from "../post-model"; import {HttpClient} from "@angular/common/http"; +import {environment} from "../../../environment/environment"; @Injectable({ providedIn: 'root' @@ -48,7 +49,7 @@ export class ArticleCreationService { sendPostCreationRequest(formData: FormData) { console.log(formData); this.http - .post('http://localhost:8080/api/v1/blog/post', formData, { + .post(`${environment.backendUrl}/blog/post`, formData, { withCredentials: true }) .subscribe({ diff --git a/EEDU-Frontend/src/app/news/news.service.ts b/EEDU-Frontend/src/app/news/news.service.ts index 63348d32..5f4e8be1 100644 --- a/EEDU-Frontend/src/app/news/news.service.ts +++ b/EEDU-Frontend/src/app/news/news.service.ts @@ -4,6 +4,7 @@ import {Router} from "@angular/router"; import {HttpClient} from "@angular/common/http"; import {Observable, tap} from "rxjs"; import {UserService} from "../user/user.service"; +import {environment} from "../../environment/environment"; @Injectable({ providedIn: 'root' @@ -11,7 +12,10 @@ import {UserService} from "../user/user.service"; export class NewsService implements OnInit { constructor(public router: Router, public http: HttpClient, public userService: UserService) { console.log("Getting posts...") - this.getPosts(); + this.getPosts().subscribe(posts => { + this.articleList = posts; + console.log(this.articleList); + }); } articleList: PostModel[] = []; @@ -27,7 +31,7 @@ export class NewsService implements OnInit { pageIndex = 0; } - return this.http.get(`http://localhost:8080/api/v1/blog/get/list?pageNumber=${pageIndex}`, { + return this.http.get(`${environment.backendUrl}/blog/get/list?pageNumber=${pageIndex}`, { withCredentials: true }).pipe( tap((list) => { @@ -39,14 +43,14 @@ export class NewsService implements OnInit { public getCount(): Observable { - return this.http.get('http://localhost:8080/api/v1/blog/get/length', { + return this.http.get(`${environment.backendUrl}/blog/get/length`, { withCredentials: true }); } public getArticle(id: number): Observable { - return this.http.get(`http://localhost:8080/api/v1/blog/get/${id}`, { + return this.http.get(`${environment.backendUrl}/blog/get/${id}`, { withCredentials: true }); } diff --git a/EEDU-Frontend/src/app/settings/settings.component.html b/EEDU-Frontend/src/app/settings/settings.component.html index e96431dd..2df26298 100644 --- a/EEDU-Frontend/src/app/settings/settings.component.html +++ b/EEDU-Frontend/src/app/settings/settings.component.html @@ -12,7 +12,8 @@

Themes


-

+

+

@@ -29,4 +30,4 @@

Feedback

Feedback -


+


diff --git a/EEDU-Frontend/src/app/settings/settings.component.scss b/EEDU-Frontend/src/app/settings/settings.component.scss index 968c5aff..dc3a106b 100644 --- a/EEDU-Frontend/src/app/settings/settings.component.scss +++ b/EEDU-Frontend/src/app/settings/settings.component.scss @@ -9,6 +9,6 @@ } .feedback-button { - background-color: transparent; + background-color: var(--widget-color); color: var(--text-color); } diff --git a/EEDU-Frontend/src/app/settings/settings.component.ts b/EEDU-Frontend/src/app/settings/settings.component.ts index e3226f63..36009862 100644 --- a/EEDU-Frontend/src/app/settings/settings.component.ts +++ b/EEDU-Frontend/src/app/settings/settings.component.ts @@ -17,6 +17,7 @@ import { MatExpansionModule, } from "@angular/material/expansion"; import {FullUserListComponent} from "../user/user-list/full-user-list/full-user-list.component"; +import {environment} from "../../environment/environment"; @Component({ selector: 'app-settings', @@ -50,8 +51,6 @@ export class SettingsComponent implements OnInit { themeForm = new FormControl(null, Validators.required); public feedbackText = ""; - public THEME_URL: string = "api/v1/user"; - ngOnInit(): void { this.themes = this.fetchAllThemes(); this.themeForm.valueChanges.subscribe((selectedTheme) => { @@ -110,7 +109,7 @@ export class SettingsComponent implements OnInit { * * @param parsedUserData Parsed object representation of the userData JSON retrieved from local storage */ - public processSettings(parsedUserData: any) { + public processSettings(parsedUserData: any): void { const theme$: Observable = this.setTheme(this.selectedTheme); console.log(theme$); const observables: Observable[] = [theme$]; // add further observables along the way @@ -130,7 +129,7 @@ export class SettingsComponent implements OnInit { * @returns Observable carrying the full newly selected theme. */ public setTheme(themeId: bigint): Observable { - const url: string = `http://localhost:8080/${this.THEME_URL}/me/theme/set`; + const url: string = `${environment.backendUrl}/user/me/theme/set`; return this.http.put(url, themeId, { withCredentials: true }).pipe(map(model => { @@ -140,6 +139,27 @@ export class SettingsComponent implements OnInit { })); } + /** + * Creates a cookie containing the received theme data. + * + * @param themeId - The theme to receive + */ + public setThemeLocally(): void + { + this.getTheme(this.selectedTheme).subscribe((themeModel: ThemeModel): void => { + let themeModelJson: string = JSON.stringify(themeModel); + document.cookie = `theme=${themeModelJson}; max-age=${60 * 60 * 24 * 365}; path=/;`; + }); + } + + public getTheme(themeId: bigint): Observable { + const url: string = `${environment.backendUrl}/user/theme/get/${themeId}`; + + return this.http.get(url, { + withCredentials: true + }); + } + /** * Fetches all themes as a SimpleThemeEntity array observable. Typically used for * a theme selection dropdown. @@ -148,7 +168,7 @@ export class SettingsComponent implements OnInit { * id, name format. */ public fetchAllThemes() : Observable { - const url: string = `http://localhost:8080/${this.THEME_URL}/theme/all`; + const url: string = `${environment.backendUrl}/user/theme/all`; return this.http.get(url, {withCredentials: true}); } diff --git a/EEDU-Frontend/src/app/theming/theme.service.ts b/EEDU-Frontend/src/app/theming/theme.service.ts index 333cbf07..7dd9c0c3 100644 --- a/EEDU-Frontend/src/app/theming/theme.service.ts +++ b/EEDU-Frontend/src/app/theming/theme.service.ts @@ -5,11 +5,7 @@ import {ThemeModel} from "./theme-model"; @Injectable({ providedIn: 'root' }) -/** - * This ThemeService provides methods to retrieve theme-related information - * for UI elements. Apart from getter methods, this service also includes text color - * logic based on the luminance of the given background for better readability. - */ + export class ThemeService { constructor(public userService: UserService) { } diff --git a/EEDU-Frontend/src/app/timetable/calendar-controls/calendar-controls.component.ts b/EEDU-Frontend/src/app/timetable/calendar-controls/calendar-controls.component.ts index 88363c16..cefbf22b 100644 --- a/EEDU-Frontend/src/app/timetable/calendar-controls/calendar-controls.component.ts +++ b/EEDU-Frontend/src/app/timetable/calendar-controls/calendar-controls.component.ts @@ -70,10 +70,10 @@ export class CalendarControlsComponent { protected get title(): string { switch (this._viewType) { case CalendarView.Month: - return this.viewDate.toLocaleDateString('de-DE', {month: 'long', year: 'numeric'}); + return this.viewDate.toLocaleDateString('en-GB', {month: 'long', year: 'numeric'}); case CalendarView.Day: - return this.viewDate.toLocaleDateString('de-DE', { + return this.viewDate.toLocaleDateString('en-GB', { weekday: 'long', month: 'long', day: 'numeric', @@ -89,10 +89,10 @@ export class CalendarControlsComponent { startOfWeek.setDate(this.viewDate.getDate() + daysToMonday); // Go to Monday endOfWeek.setDate(startOfWeek.getDate() + 6); // Go to Sunday (end of the week) - return `${startOfWeek.toLocaleDateString('de-DE', { + return `${startOfWeek.toLocaleDateString('en-GB', { month: 'short', day: 'numeric' - })} – ${endOfWeek.toLocaleDateString('de-DE', {month: 'short', day: 'numeric', year: 'numeric'})}`; + })} – ${endOfWeek.toLocaleDateString('en-GB', {month: 'short', day: 'numeric', year: 'numeric'})}`; } } diff --git a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-student-view/assignment-student-view.component.html b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-student-view/assignment-student-view.component.html index af098343..f51d8d8c 100644 --- a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-student-view/assignment-student-view.component.html +++ b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-student-view/assignment-student-view.component.html @@ -4,12 +4,7 @@ assignment_turned_in
-
- {{ insight.submitted }} - {{ insight.files }} -
- -
+
Deadline: @@ -17,10 +12,12 @@ -
- + +
+ + diff --git a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-student-view/assignment-student-view.component.ts b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-student-view/assignment-student-view.component.ts index 2382716d..d5b0603b 100644 --- a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-student-view/assignment-student-view.component.ts +++ b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-student-view/assignment-student-view.component.ts @@ -6,9 +6,10 @@ import {MatButton} from "@angular/material/button"; import {MatList, MatListItem, MatListItemLine, MatListItemTitle} from "@angular/material/list"; import {NgIf} from "@angular/common"; import {MatIcon} from "@angular/material/icon"; -import {AssignmentModel} from "../../../../user/courses/appointment/entry/assignment-model"; -import {AppointmentService} from "../../../../user/courses/appointment/appointment.service"; -import {AssignmentInsightModel} from "../../../../user/courses/appointment/entry/assignment-insight-model"; +import {AssignmentService} from "../../../../user/courses/appointment/entry/assignment/assignment.service"; +import {AssignmentInsightModel} from "../../../../user/courses/appointment/entry/assignment/assignment-insight-model"; +import {AssignmentModel} from "../../../../user/courses/appointment/entry/assignment/assignment-model"; +import {InsightListComponent} from "../insight-list/insight-list.component"; @Component({ selector: 'app-assignment-student-view', @@ -21,7 +22,8 @@ import {AssignmentInsightModel} from "../../../../user/courses/appointment/entry MatListItemTitle, MatListItem, NgIf, - MatIcon + MatIcon, + InsightListComponent, ], templateUrl: './assignment-student-view.component.html', styleUrl: './assignment-student-view.component.scss' @@ -34,16 +36,16 @@ export class AssignmentStudentViewComponent implements AfterViewInit { private _insight?: AssignmentInsightModel; - protected get insight(): AssignmentInsightModel | undefined { - return this._insight; + protected get insight(): AssignmentInsightModel | null { + return this._insight || null; } - public constructor(private readonly _appointmentService: AppointmentService, form: FormBuilder) { + public constructor(private readonly _assignmentService: AssignmentService, form: FormBuilder) { this._form = form.group({files: [[], Validators.required]}); } public ngAfterViewInit(): void { - this._appointmentService.fetchInsight(this.appointment.id).subscribe((insights: AssignmentInsightModel): void => { + this._assignmentService.fetchInsight(this.appointment.id).subscribe((insights: AssignmentInsightModel): void => { this._insight = insights; }); } @@ -65,7 +67,7 @@ export class AssignmentStudentViewComponent implements AfterViewInit { } // -- - this._appointmentService.submitAssignment(this.appointment.id, fileArray).subscribe((): void => {}); + this._assignmentService.submitAssignment(this.appointment.id, fileArray).subscribe((): void => {}); } protected onFileSelected(event: FileList): void { diff --git a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.html b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.html index a5a558d0..888d2b46 100644 --- a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.html +++ b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.html @@ -4,14 +4,49 @@ - - -

Submitted {{ currentInsight.submitted }}

+

+ {{ assignment?.description }} +

- - @for (file of currentInsight.files; track file) { - {{ file }} + + Current User + + @for (insight of assignmentInsight; track assignmentInsight) { + + {{ toIcon(insight) }} + {{ insight.user.name }} + } - + + + + + +
+
+ + + +
+ + Feedback + + + +
+
+
+ +
+ + +
diff --git a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.scss b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.scss index e69de29b..ce3ff15c 100644 --- a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.scss +++ b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.scss @@ -0,0 +1,3 @@ +.mat-list .mat-list-item { + height: 50px; /* default is 72px */ +} diff --git a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.ts b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.ts index 6f0ee027..9f5cd2f3 100644 --- a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.ts +++ b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/assignment-teacher-view/assignment-teacher-view.component.ts @@ -1,43 +1,99 @@ import {Component, Input, input, InputSignal} from '@angular/core'; import {NgIf} from "@angular/common"; import {AppointmentEntryModel} from "../../../../user/courses/appointment/entry/appointment-entry-model"; -import {AssignmentModel} from "../../../../user/courses/appointment/entry/assignment-model"; -import {SelectionInput} from "../../../../common/selection-input/selection-input.component"; -import {AppointmentService} from "../../../../user/courses/appointment/appointment.service"; -import {AssignmentInsightModel} from "../../../../user/courses/appointment/entry/assignment-insight-model"; -import {MatList, MatListItem} from "@angular/material/list"; +import {AssignmentInsightModel} from "../../../../user/courses/appointment/entry/assignment/assignment-insight-model"; +import {AssignmentService} from "../../../../user/courses/appointment/entry/assignment/assignment.service"; +import {AssignmentModel} from "../../../../user/courses/appointment/entry/assignment/assignment-model"; +import {MatFormField, MatLabel} from "@angular/material/form-field"; +import {MatOption, MatSelect} from "@angular/material/select"; +import {MatIcon} from "@angular/material/icon"; +import {MatButton} from "@angular/material/button"; +import {MatInput} from "@angular/material/input"; +import {FormBuilder, FormGroup, ReactiveFormsModule} from "@angular/forms"; +import {AssessmentService} from "../../../../user/courses/appointment/entry/assignment/assessment/assessment.service"; +import {UserService} from "../../../../user/user.service"; +import {AssessmentModel} from "../../../../user/courses/appointment/entry/assignment/assessment/assessment-model"; +import { + AssessmentCreateModel +} from "../../../../user/courses/appointment/entry/assignment/assessment/assessment-create-model"; +import {GeneralErrorBoxComponent} from "../../../../common/general-error-box/general-error-box.component"; +import {InsightListComponent} from "../insight-list/insight-list.component"; @Component({ selector: 'app-assignment-teacher-view', standalone: true, - imports: [ - NgIf, - SelectionInput, - MatList, - MatListItem - ], + imports: [NgIf, MatLabel, MatFormField, MatSelect, MatOption, MatIcon, MatButton, MatInput, ReactiveFormsModule, GeneralErrorBoxComponent, InsightListComponent], templateUrl: './assignment-teacher-view.component.html', styleUrl: './assignment-teacher-view.component.scss' }) export class AssignmentTeacherViewComponent { private _appointment: AppointmentEntryModel | null = null; - private _assignmentInsightModels: readonly AssignmentInsightModel[] = []; + private _assignmentInsight: readonly AssignmentInsightModel[] = []; + private _insight: AssignmentInsightModel | null = null; public readonly editing: InputSignal = input(false); - private _currentInsight: AssignmentInsightModel | null = null; + private readonly _assessForm: FormGroup; + public constructor( + private readonly _assignmentService: AssignmentService, + private readonly _assessmentService: AssessmentService, + private readonly _userService: UserService, + formBuilder: FormBuilder) { + this._assessForm = formBuilder.group({ + feedback: [null] + }) + } + + protected get assessForm(): FormGroup { + return this._assessForm; + } - public constructor(private readonly _appointmentService: AppointmentService) { + protected set currentInsight(assignmentInsight: AssignmentInsightModel) + { + this._insight = assignmentInsight; + } + + protected get currentInsight(): AssignmentInsightModel | null + { + return this._insight; } - @Input() - public set appointment(appointment: AppointmentEntryModel) + protected onAssess(): void { + if(!this.currentInsight) + { + return; + } + + const insight: AssignmentInsightModel = this.currentInsight; + this._assessmentService.assess([AssessmentCreateModel.fromObject({ + appointment: Number(this.appointment?.id), + user: insight.user.id, + feedback: this.assessForm.get('feedback')?.value, + })]).subscribe((assessmentModel: readonly AssessmentModel[]): void => { + this._assignmentInsight.map((current: AssignmentInsightModel): AssignmentInsightModel => { + + if(current === insight) + { + const newInsight: AssignmentInsightModel = AssignmentInsightModel.pushAssessment(current, assessmentModel[0]); + if(this.currentInsight === insight) + { + this.currentInsight = newInsight; + } + return newInsight; + } + + return current; + }) + }); + } + + @Input() public set appointment(appointment: AppointmentEntryModel) { this._appointment = appointment; - this._appointmentService.fetchInsights(appointment.id).subscribe((response: AssignmentInsightModel[]): void => + this._assignmentService.fetchInsights(appointment.id).subscribe((response: AssignmentInsightModel[]): void => { - this._assignmentInsightModels = response; + this._assignmentInsight = response; }) } @@ -45,19 +101,18 @@ export class AssignmentTeacherViewComponent { return this._appointment; } - protected get assignmentInsightModels(): readonly AssignmentInsightModel[] { - return this._assignmentInsightModels; + protected get assignmentInsight(): readonly AssignmentInsightModel[] { + return this._assignmentInsight; } protected get assignment(): AssignmentModel | null { return this.appointment!.assignment || null; } - protected valueUpdate(event: readonly AssignmentInsightModel[] | AssignmentInsightModel): void { - this._currentInsight = event as AssignmentInsightModel - } - - protected get currentInsight(): AssignmentInsightModel | null { - return this._currentInsight; + protected toIcon(insight: AssignmentInsightModel): 'assignment_turned_in' | 'assignment_late' | 'pending' { + if (this.assignment?.submitUntil.getTime() && (this.assignment?.submitUntil.getTime()) < new Date().getTime()) { + return insight.submitted ? 'assignment_turned_in' : 'assignment_late'; + } + return insight.submitted ? 'assignment_turned_in' : 'pending'; } } diff --git a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.html b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.html new file mode 100644 index 00000000..afd7c9ee --- /dev/null +++ b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.html @@ -0,0 +1,11 @@ +
+
    + @for (file of insight()!.files || []; track file) { +
  • {{ file }}
  • + } +
+ + +

{{ insight()!.assessment?.feedback }}

+
+
diff --git a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.scss b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.spec.ts b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.spec.ts new file mode 100644 index 00000000..ec373c46 --- /dev/null +++ b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InsightListComponent } from './insight-list.component'; + +describe('InsightListComponent', () => { + let component: InsightListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InsightListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(InsightListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.ts b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.ts new file mode 100644 index 00000000..a7bd7eee --- /dev/null +++ b/EEDU-Frontend/src/app/timetable/event-data/assignment-tab/insight-list/insight-list.component.ts @@ -0,0 +1,18 @@ +import {Component, input, InputSignal} from '@angular/core'; +import {AssignmentInsightModel} from "../../../../user/courses/appointment/entry/assignment/assignment-insight-model"; +import {NgIf} from "@angular/common"; + +@Component({ + selector: 'app-insight-list', + imports: [ + NgIf + ], + templateUrl: './insight-list.component.html', + styleUrl: './insight-list.component.scss' +}) +export class InsightListComponent { + + public readonly insight: InputSignal = input(null); + + +} diff --git a/EEDU-Frontend/src/app/timetable/event-data/event-data-dialog.component.ts b/EEDU-Frontend/src/app/timetable/event-data/event-data-dialog.component.ts index 2f7bc0b6..bd34d671 100644 --- a/EEDU-Frontend/src/app/timetable/event-data/event-data-dialog.component.ts +++ b/EEDU-Frontend/src/app/timetable/event-data/event-data-dialog.component.ts @@ -10,8 +10,6 @@ import {MatGridList, MatGridTile} from "@angular/material/grid-list"; import {MatButton} from "@angular/material/button"; import {MatFormField, MatHint} from "@angular/material/form-field"; import {MatInput} from "@angular/material/input"; -import {AssignmentModel} from "../../user/courses/appointment/entry/assignment-model"; -import {GenericAssignmentCreateModel} from "../../user/courses/appointment/entry/assignment-create-model"; import {AssignmentTabComponent} from "./assignment-tab/assignment-tab.component"; import { DateTimePickerComponent @@ -20,35 +18,18 @@ import {EventTileContentComponent} from "./event-tile-content/event-tile-content import {RoomTabComponent} from "./room-tab/room-tab.component"; import {SelectionInput} from "../../common/selection-input/selection-input.component"; import { - MAT_DIALOG_DATA, - MatDialogActions, - MatDialogContent, + MAT_DIALOG_DATA, MatDialogActions, MatDialogContent, } from "@angular/material/dialog"; import {MatSnackBar} from "@angular/material/snack-bar"; import {CourseService} from "../../user/courses/course.service"; import {CourseModel} from "../../user/courses/course-model"; import {GeneralCardComponent} from "../../common/general-card-component/general-card.component"; +import {AssignmentModel} from "../../user/courses/appointment/entry/assignment/assignment-model"; +import {GenericAssignmentCreateModel} from "../../user/courses/appointment/entry/assignment/assignment-create-model"; @Component({ standalone: true, - imports: [ - ReactiveFormsModule, - NgIf, - MatHint, - MatGridList, - MatGridTile, - MatFormField, - MatInput, - MatButton, - AssignmentTabComponent, - DateTimePickerComponent, - EventTileContentComponent, - RoomTabComponent, - SelectionInput, - MatDialogContent, - MatDialogActions, - GeneralCardComponent - ], + imports: [ReactiveFormsModule, NgIf, MatHint, MatGridList, MatGridTile, MatFormField, MatInput, MatButton, AssignmentTabComponent, DateTimePickerComponent, EventTileContentComponent, RoomTabComponent, SelectionInput, MatDialogContent, MatDialogActions, GeneralCardComponent], templateUrl: './event-data-dialog.component.html', styleUrl: './event-data-dialog.component.scss' }) @@ -58,14 +39,10 @@ export class EventDataDialogComponent { private readonly _form: FormGroup; private readonly _rooms: RoomModel[] = []; - public constructor( - formBuilder: FormBuilder, - roomService: RoomService, - @Inject(MAT_DIALOG_DATA) data: { title: string, appointment: AppointmentEntryModel }, - private readonly _appointmentService: AppointmentService, - private readonly _courseService: CourseService, - private readonly _matSnackBar: MatSnackBar, - ) { + public constructor(formBuilder: FormBuilder, roomService: RoomService, @Inject(MAT_DIALOG_DATA) data: { + title: string, + appointment: AppointmentEntryModel + }, private readonly _appointmentService: AppointmentService, private readonly _courseService: CourseService, private readonly _matSnackBar: MatSnackBar,) { roomService.value$.subscribe((rooms: RoomModel[]): void => { this._rooms.length = 0; this._rooms.push(...rooms); @@ -76,20 +53,12 @@ export class EventDataDialogComponent { console.log(data.appointment) this._form = formBuilder.group({ - description: [null], - room: [null], - assignment: [null], - publish: [null], - submitUntil: [null] + description: [null], room: [null], assignment: [null], publish: [null], submitUntil: [null] }); this.appointment = data.appointment; } - protected get course(): CourseModel { - return this._courseService.findCourseLazily(this.event.course) as CourseModel; // expect the course to exist - } - @Input() public set appointment(value: AppointmentEntryModel) { this._event = value; @@ -106,16 +75,20 @@ export class EventDataDialogComponent { // Default values when creating a new assignment this.form.get('publish')?.setValue(new Date()); + this.form.get('submitUntil')?.setValue(new Date(new Date().getTime() + (1000 * 60 * 60 * 24 * 7))); - // TODO also include frequent appointments - const appointments: readonly AppointmentEntryModel[] = this._appointmentService.nextAppointments; - let start: Date = new Date(new Date().getTime() + (1000 * 60 * 60 * 24 * 7)); - if(appointments.length !== 0) + this._appointmentService.nextAppointments.subscribe((appointments: readonly AppointmentEntryModel[]): void => { - start = appointments[0].start; - } + if(appointments.length === 0) + { + return; + } + this.form.get('submitUntil')?.setValue(appointments[0].start); + }) + } - this.form.get('submitUntil')?.setValue(start); + protected get course(): CourseModel { + return this._courseService.findCourseLazily(this.event.course) as CourseModel; // expect the course to exist } protected get title(): string { @@ -140,18 +113,10 @@ export class EventDataDialogComponent { return this._form; } - protected get assignmentModel(): AssignmentModel { - return AssignmentModel.fromObject({ - description: this.form.get('assignment')?.value, - publish: this.form.get('publish')?.value, - submitUntil: this.form.get('submitUntil')?.value - }); - } - private get assignmentCreateModel(): GenericAssignmentCreateModel { + // if the 'assignment' field is undefined, these field below will be ignored return { description: this.form.get("assignment")?.value, - // if the 'assignment' field is undefined, these will be ignored publish: (this.form.get('publish')?.value as Date), submitUntil: (this.form.get('submitUntil')?.value as Date) } @@ -178,14 +143,17 @@ export class EventDataDialogComponent { } protected onSubmit(): void { + if (!this.anyEdit) { + return; + } + this._appointmentService.updateAppointment(this.event.id, AppointmentUpdateModel.fromObject({ description: this.form.get('description')?.value, - room: this.form.get('room')?.value, - // undefined means not updating !!! + room: this.form.get('room')?.value, // undefined means not updating !!! assignment: this.hasEdited('assignment') ? this.assignmentCreateModel : undefined })).subscribe((response: AppointmentEntryModel): void => { this.appointment = response; - this._matSnackBar.open("The changes have been saved!", "", { duration: 2000 }); + this._matSnackBar.open("The changes have been saved!", "", {duration: 2000}); }); } } diff --git a/EEDU-Frontend/src/app/timetable/timetable.component.ts b/EEDU-Frontend/src/app/timetable/timetable.component.ts index edfa8697..15185003 100644 --- a/EEDU-Frontend/src/app/timetable/timetable.component.ts +++ b/EEDU-Frontend/src/app/timetable/timetable.component.ts @@ -71,6 +71,11 @@ export class TimetableComponent implements OnInit, OnDestroy { } private set selectedEvent(value: CalendarEvent | undefined) { + if(value?.meta.eventData instanceof FrequentAppointmentModel) + { + this.createEvent(value?.start, value?.meta.eventData as FrequentAppointmentModel); + } + this._selectedEvent = value; } @@ -150,14 +155,10 @@ export class TimetableComponent implements OnInit, OnDestroy { this.document.body.classList.remove(this.CALENDAR_THEME_CLASS) } - protected createEvent(): void { - if (!this.selectedEvent) { - return; - } + protected createEvent(start: Date, eventData: FrequentAppointmentModel): void { - const frequentData: FrequentAppointmentModel = this.selectedEvent.meta.eventData as FrequentAppointmentModel; - this._appointmentService.createAppointment(frequentData.course.id, [AppointmentCreateModel.fromObject({ - start: this.selectedEvent.start, room: frequentData.room, duration: frequentData.duration, + this._appointmentService.createAppointment(eventData.course.id, [AppointmentCreateModel.fromObject({ + start: start, room: eventData.room, duration: eventData.duration, })]).subscribe((createdEvent: AppointmentEntryModel[]): void => { this.selectedEvent = this.events.find((current: CalendarEvent): boolean => { return typeof current.id === 'number' && BigInt(current.id) === createdEvent[0].id; diff --git a/EEDU-Frontend/src/app/user/authentication/authentication.component.html b/EEDU-Frontend/src/app/user/authentication/authentication.component.html index 88a4f65a..ef6eae46 100644 --- a/EEDU-Frontend/src/app/user/authentication/authentication.component.html +++ b/EEDU-Frontend/src/app/user/authentication/authentication.component.html @@ -1,5 +1,5 @@
- +