Skip to content
This repository was archived by the owner on Sep 10, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
986a6c9
Implemented possibility to delete submitted assignments
Plugrol Mar 14, 2025
3fd4957
Populated news card, not finished
SortyFix Mar 14, 2025
3abcc7c
Outsourced illness notification requests to service and removed all u…
SortyFix Mar 14, 2025
44a5ec7
Saved Ivo's mental health
SortyFix Mar 14, 2025
ffdc5e9
Fixed some issues
Plugrol Mar 14, 2025
e7ee023
Fixed endpoint issues
SortyFix Mar 14, 2025
a837a7f
Made some qol
Plugrol Mar 14, 2025
2cb97be
Finished news card
SortyFix Mar 16, 2025
1f4fb45
Removed obsolete code
SortyFix Mar 16, 2025
e167463
Enhanced user experience
SortyFix Mar 17, 2025
65a9ce3
Removed imports
SortyFix Mar 17, 2025
c8f531b
did some things
Plugrol Mar 17, 2025
fe572d6
Merge branch 'implement-dashboard' into assessments
Plugrol Mar 17, 2025
d734c4c
Renamed default themes
SortyFix Mar 17, 2025
acb17af
Fixed some issues
Plugrol Mar 17, 2025
36f9cb1
Added possibility to assess
Plugrol Mar 17, 2025
a03f305
Removed BS
SortyFix Mar 18, 2025
c7e93b7
Removed grades. I am not going to deal with this
Plugrol Mar 19, 2025
f3d9cba
Implemented some more assessment stuff
Plugrol Mar 19, 2025
91ba174
Adjusted some minor things
Plugrol Mar 23, 2025
f40fce0
Use SHA256 instead of SHA1
Plugrol Mar 23, 2025
3b572ba
Minor changes
SortyFix Mar 25, 2025
9f024a1
Made Frequent Appointments creatable again
Plugrol Mar 25, 2025
959eb43
Fixed themes
SortyFix Mar 25, 2025
2fa2ba3
Moved from post to put
Plugrol Mar 25, 2025
d16a3d2
Merge branch 'implement-dashboard' into assessments
Plugrol Mar 25, 2025
70fffba
Fixed login page
SortyFix Mar 25, 2025
46f72c5
Fixed drop down menu theme in course creator
SortyFix Mar 25, 2025
1b653bf
Made students able to see feedback
Plugrol Mar 28, 2025
393d841
Merge branch 'implement-dashboard' into assessments
Plugrol Mar 28, 2025
c7b2e3d
Fixed some styles
Plugrol Mar 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions EEDU-Backend/src/main/java/de/gaz/eedu/DataLoader.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Long, PostEntity>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public class CourseController extends EntityController<Long, CourseService, Cour
{
private final CourseService service;

@GetMapping("/{course}/subject/{subject}")
@PutMapping("/{course}/subject/{subject}")
@PreAuthorize("hasAuthority(T(de.gaz.eedu.user.privileges.SystemPrivileges).COURSE_ALTER_SUBJECT.toString())")
public @NotNull ResponseEntity<Void> setSubject(@PathVariable long course, @PathVariable String subject)
{
log.info("Received incoming request for setting the subject of course {} to {}.", course, subject);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -54,42 +51,6 @@ public class AppointmentController extends EntityController<Long, AppointmentSer
return ResponseEntity.ok(entities.map(FrequentAppointmentEntity::toModel).toArray(FrequentAppointmentModel[]::new));
}

@PreAuthorize("hasRole('teacher')")
@GetMapping("/assignment/{appointment}/status/all")
public @NotNull ResponseEntity<AssignmentInsightModel[]> submitStatus(@PathVariable long appointment)
{
return ResponseEntity.ok(getService().getInsight(appointment).toArray(AssignmentInsightModel[]::new));
}

@PreAuthorize("hasRole('teacher')")
@GetMapping("/assignment/{appointment}/status/{user}")
public @NotNull ResponseEntity<AssignmentInsightModel> submitStatus(@PathVariable long appointment, @PathVariable long user)
{
ResponseEntity<AssignmentInsightModel> notFound = ResponseEntity.notFound().build();
return getService().getInsight(appointment, user).map(ResponseEntity::ok).orElse(notFound);
}

@PreAuthorize("hasRole('student')")
@GetMapping("/assignment/{appointment}/status")
public @NotNull ResponseEntity<AssignmentInsightModel> ownSubmitStatus(@AuthenticationPrincipal long userId, @PathVariable long appointment)
{
ResponseEntity<AssignmentInsightModel> notFound = ResponseEntity.notFound().build();
return getService().getInsight(appointment, userId).map(ResponseEntity::ok).orElse(notFound);
}

@DeleteMapping("/assignment/{appointment}/delete/{files}")
public @NotNull ResponseEntity<Void> 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<Void> 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<AppointmentEntryModel> updateAppointment(@PathVariable long appointment, @NotNull @RequestBody AppointmentUpdateModel updateModel)
{
Expand All @@ -102,13 +63,12 @@ public class AppointmentController extends EntityController<Long, AppointmentSer
{
log.info("Received incoming request for unscheduling frequent appointment(s) {} from course {}.", appointments, course);
boolean modified = getService().unscheduleFrequent(course, appointments);
return empty(modified ? HttpStatus.OK : HttpStatus.NOT_MODIFIED);
return empty(modified ? HttpStatus.OK : HttpStatus.CONFLICT);
}

@PostMapping("/{course}/schedule/standalone") @PreAuthorize("hasRole('teacher') or hasRole('administrator')")
public @NotNull ResponseEntity<AppointmentEntryModel[]> setAppointment(@PathVariable long course, @RequestBody @NotNull AppointmentEntryCreateModel... createModel)
@PutMapping("/{course}/schedule/standalone") @PreAuthorize("hasRole('teacher') or hasRole('administrator')")
public @NotNull ResponseEntity<AppointmentEntryModel[]> scheduleAppointment(@PathVariable long course, @RequestBody @NotNull AppointmentEntryCreateModel... createModel)
{

List<AppointmentEntryModel> createdEntities = getService().createAppointment(course, Set.of(createModel));
return ResponseEntity.ok(createdEntities.toArray(AppointmentEntryModel[]::new));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,16 +16,20 @@
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;
import lombok.RequiredArgsConstructor;
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;
Expand All @@ -39,6 +45,7 @@
@RequiredArgsConstructor
public class AppointmentService extends EntityService<Long, FrequentAppointmentRepository, FrequentAppointmentEntity, FrequentAppointmentModel, InternalFrequentAppointmentCreateModel>
{
private final FileService fileService;
private final FrequentAppointmentRepository repository;
@Getter(AccessLevel.PUBLIC)
private final AppointmentEntryRepository entryRepository;
Expand Down Expand Up @@ -113,9 +120,8 @@ private static boolean setAssignment(@NotNull AppointmentEntryEntity entity, @Nu
Function<RoomEntity, Boolean> 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()))
);
}

Expand Down Expand Up @@ -217,6 +223,30 @@ private AppointmentEntryEntity getAppointmentEntry(long id) throws EntityUnknown
return entryReference.orElseThrow(entityUnknown(id));
}

public @NotNull ResponseEntity<ByteArrayResource> downloadAssignments(long appointment, long user)
{
AppointmentEntryEntity entry = getEntryRepository().findById(appointment).orElseThrow(entityUnknown(appointment));
return getFileService().zipAndSend(entry.loadAssignmentFiles(user));
}

public @NotNull ResponseEntity<ByteArrayResource> 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.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -55,8 +55,8 @@ public class AppointmentEntryEntity implements EntityModelRelation<Long, Appoint
private Instant publish;
@Column(name = "description", length = 1000) private String description;

@ManyToOne @JoinColumn(name = "course_appointment_id", nullable = false) @JsonBackReference
@Cascade(CascadeType.ALL) private CourseEntity course;
@ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "course_appointment_id", nullable = false) @JsonBackReference
private CourseEntity course;
@ManyToOne @JoinColumn(name = "frequent_appointment_id") @JsonBackReference
private @Nullable FrequentAppointmentEntity frequentAppointment;

Expand All @@ -68,6 +68,9 @@ public class AppointmentEntryEntity implements EntityModelRelation<Long, Appoint
@ManyToOne @JsonManagedReference @JoinColumn(name = "room_id", referencedColumnName = "id")
private @Nullable RoomEntity room;

@OneToMany(mappedBy = "appointment", cascade = CascadeType.ALL) @JsonBackReference
private final Set<AssessmentEntity> assessments = new HashSet<>();

/**
* This constructor creates a new instance of this entity.
* <p>
Expand Down Expand Up @@ -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<File> 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())
Expand All @@ -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());
}

Expand All @@ -183,6 +206,12 @@ public boolean deleteAssignment(long user, String @NotNull ... files)
return allDeleted;
}

private @NotNull Optional<AssessmentEntity> getAssessment(@NotNull UserEntity user)
{
Predicate<AssessmentEntity> userEquals = current -> Objects.equals(user, current.getUser());
return getAssessments().stream().filter(userEquals).findFirst();
}

private @NotNull String getUploadPath(long user)
{
FileEntity repository = getCourse().getRepository();
Expand All @@ -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)
Expand Down
Loading