Skip to content
This repository was archived by the owner on Sep 10, 2025. It is now read-only.

Commit 53a63b9

Browse files
committed
Implemented possibility to delete submitted assignments
1 parent f37f831 commit 53a63b9

4 files changed

Lines changed: 127 additions & 44 deletions

File tree

EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentController.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,40 +49,45 @@ public class AppointmentController extends EntityController<Long, AppointmentSer
4949
appointments,
5050
course);
5151

52-
Set<InternalFrequentAppointmentCreateModel> internal = toInternalCreateModel(course, appointments).collect(
53-
Collectors.toUnmodifiableSet());
52+
Set<InternalFrequentAppointmentCreateModel> internal = toInternalCreateModel(course, appointments).collect(Collectors.toUnmodifiableSet());
5453
Stream<FrequentAppointmentEntity> entities = getService().createEntity(internal).stream();
5554
return ResponseEntity.ok(entities.map(FrequentAppointmentEntity::toModel).toArray(FrequentAppointmentModel[]::new));
5655
}
5756

5857
@PreAuthorize("hasRole('teacher')")
59-
@GetMapping("/submit/assignment/{appointment}/status/all")
58+
@GetMapping("/assignment/{appointment}/status/all")
6059
public @NotNull ResponseEntity<AssignmentInsightModel[]> submitStatus(@PathVariable long appointment)
6160
{
6261
return ResponseEntity.ok(getService().getInsight(appointment).toArray(AssignmentInsightModel[]::new));
6362
}
6463

6564
@PreAuthorize("hasRole('teacher')")
66-
@GetMapping("/submit/assignment/{appointment}/status/{user}")
65+
@GetMapping("/assignment/{appointment}/status/{user}")
6766
public @NotNull ResponseEntity<AssignmentInsightModel> submitStatus(@PathVariable long appointment, @PathVariable long user)
6867
{
6968
ResponseEntity<AssignmentInsightModel> notFound = ResponseEntity.notFound().build();
7069
return getService().getInsight(appointment, user).map(ResponseEntity::ok).orElse(notFound);
7170
}
7271

7372
@PreAuthorize("hasRole('student')")
74-
@GetMapping("/submit/assignment/{appointment}/status")
73+
@GetMapping("/assignment/{appointment}/status")
7574
public @NotNull ResponseEntity<AssignmentInsightModel> ownSubmitStatus(@AuthenticationPrincipal long userId, @PathVariable long appointment)
7675
{
7776
ResponseEntity<AssignmentInsightModel> notFound = ResponseEntity.notFound().build();
7877
return getService().getInsight(appointment, userId).map(ResponseEntity::ok).orElse(notFound);
7978
}
8079

81-
@PostMapping("/submit/assignment/{appointment}")
80+
@DeleteMapping("/assignment/{appointment}/delete/{files}")
81+
public @NotNull ResponseEntity<Void> deleteAssignment(@AuthenticationPrincipal long userId, @PathVariable long appointment, @PathVariable @NotNull String[] files)
82+
{
83+
return empty(getService().deleteAssignment(userId, appointment, files) ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR);
84+
}
85+
86+
@PostMapping("/assignment/{appointment}/submit")
8287
public @NotNull ResponseEntity<Void> submitAssignment(@AuthenticationPrincipal long userId, @PathVariable long appointment, @NotNull @RequestPart("file") MultipartFile[] files)
8388
{
8489
getService().submitAssignment(userId, appointment, files);
85-
return empty(HttpStatus.OK);
90+
return empty(HttpStatus.CREATED);
8691
}
8792

8893
@PostMapping("/update/standalone/{appointment}") @PreAuthorize("hasRole('teacher') or hasRole('administrator')")

EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/AppointmentService.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,23 @@ public boolean unscheduleFrequent(long courseId, @NotNull Long... entities)
198198

199199
public void submitAssignment(long user, long assignment, @NotNull MultipartFile[] files)
200200
{
201-
Optional<AppointmentEntryEntity> entryReference = getEntryRepository().findById(assignment);
202-
AppointmentEntryEntity entry = entryReference.orElseThrow(entityUnknown(assignment));
203-
entry.submitAssignment(user, files);
201+
getAppointmentEntry(assignment).submitAssignment(user, files);
202+
}
203+
204+
public boolean deleteAssignment(long user, long assignment, @NotNull String[] files)
205+
{
206+
return getAppointmentEntry(assignment).deleteAssignment(user, files);
204207
}
205208

206209
public void hasSubmitted(long user, long assignment, @NotNull MultipartFile[] files)
207210
{
208-
Optional<AppointmentEntryEntity> entryReference = getEntryRepository().findById(assignment);
209-
AppointmentEntryEntity entry = entryReference.orElseThrow(entityUnknown(assignment));
210-
entry.submitAssignment(user, files);
211+
getAppointmentEntry(assignment).submitAssignment(user, files);
212+
}
213+
214+
private AppointmentEntryEntity getAppointmentEntry(long id) throws EntityUnknownException
215+
{
216+
Optional<AppointmentEntryEntity> entryReference = getEntryRepository().findById(id);
217+
return entryReference.orElseThrow(entityUnknown(id));
211218
}
212219

213220
/**

EEDU-Backend/src/main/java/de/gaz/eedu/course/appointment/entry/AppointmentEntryEntity.java

Lines changed: 93 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
import org.springframework.web.server.ResponseStatusException;
3434

3535
import java.io.File;
36+
import java.io.IOException;
37+
import java.nio.file.LinkOption;
38+
import java.nio.file.Path;
39+
import java.nio.file.Paths;
3640
import java.time.Duration;
3741
import java.time.Instant;
3842
import java.util.Arrays;
@@ -49,19 +53,17 @@ public class AppointmentEntryEntity implements EntityModelRelation<Long, Appoint
4953
@Setter(AccessLevel.NONE) @Id private Long id;
5054
private Duration duration;
5155
private Instant publish;
52-
@Column(name = "description", length = 1000)
53-
private String description;
56+
@Column(name = "description", length = 1000) private String description;
5457

55-
@ManyToOne @JoinColumn(name = "course_appointment_id", nullable = false) @JsonBackReference @Cascade(CascadeType.ALL)
56-
private CourseEntity course;
58+
@ManyToOne @JoinColumn(name = "course_appointment_id", nullable = false) @JsonBackReference
59+
@Cascade(CascadeType.ALL) private CourseEntity course;
5760
@ManyToOne @JoinColumn(name = "frequent_appointment_id") @JsonBackReference
5861
private @Nullable FrequentAppointmentEntity frequentAppointment;
5962

6063
// must be set through extra method to validate integrity
61-
@Getter(AccessLevel.PROTECTED) @Setter(AccessLevel.NONE)
62-
@Nullable private String assignmentDescription;
63-
@Getter(AccessLevel.PROTECTED) @Setter(AccessLevel.NONE)
64-
@Nullable private Instant publishAssignment, submitAssignmentUntil;
64+
@Getter(AccessLevel.PROTECTED) @Setter(AccessLevel.NONE) @Nullable private String assignmentDescription;
65+
@Getter(AccessLevel.PROTECTED) @Setter(AccessLevel.NONE) @Nullable
66+
private Instant publishAssignment, submitAssignmentUntil;
6567

6668
@ManyToOne @JsonManagedReference @JoinColumn(name = "room_id", referencedColumnName = "id")
6769
private @Nullable RoomEntity room;
@@ -80,6 +82,42 @@ public AppointmentEntryEntity(long id)
8082
this.id = id;
8183
}
8284

85+
private static @NotNull File loadFileSave(@NotNull String uploadPath, @NotNull String fileName)
86+
{
87+
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\"))
88+
{
89+
String errorMessage = String.format("Invalid file name: %s contains illegal path characters.", fileName);
90+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, errorMessage);
91+
}
92+
93+
Path filePath = Paths.get(uploadPath, fileName);
94+
95+
try
96+
{
97+
Path normalizedPath = filePath.toRealPath(LinkOption.NOFOLLOW_LINKS);
98+
99+
// some security is crucial
100+
if (!normalizedPath.startsWith(uploadPath))
101+
{
102+
String errorMessage = String.format("File %s is outside the allowed directory.", fileName);
103+
throw new ResponseStatusException(HttpStatus.FORBIDDEN, errorMessage);
104+
}
105+
106+
File file = normalizedPath.toFile();
107+
if (!file.exists())
108+
{
109+
String errorMessage = String.format("File %s does not exist.", fileName);
110+
throw new ResponseStatusException(HttpStatus.NOT_FOUND, errorMessage);
111+
}
112+
113+
return file;
114+
} catch (IOException e)
115+
{
116+
String errorMessage = String.format("Error processing file path for %s.", fileName);
117+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, errorMessage, e);
118+
}
119+
}
120+
83121
public @NotNull Optional<RoomEntity> getRoom()
84122
{
85123
FrequentAppointmentEntity frequentAppointment = getFrequentAppointment();
@@ -92,8 +130,7 @@ public AppointmentEntryEntity(long id)
92130

93131
public @NotNull AssignmentInsightModel getInsight(@NotNull UserEntity user)
94132
{
95-
FileEntity repository = getCourse().getRepository();
96-
String uploadPath = repository.getFilePath(uploadPath(user.getId()));
133+
String uploadPath = getUploadPath(user.getId());
97134
File file = new File(uploadPath);
98135
File[] files = file.listFiles();
99136
if (!hasSubmitted(user) || !file.isDirectory() || Objects.isNull(files) || files.length == 0)
@@ -115,8 +152,41 @@ public void submitAssignment(long user, @NotNull MultipartFile... files) throws
115152
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY);
116153
}
117154

118-
getCourse().getRepository().uploadBatch(uploadPath(user), files);
119-
log.info("User {} has uploaded files to appointment entry {}", user, getId());
155+
String uploadPath = getUploadPath(user);
156+
File[] file = new File(uploadPath).listFiles();
157+
if (file != null && (file.length + files.length) > 5)
158+
{
159+
throw new ResponseStatusException(HttpStatus.PAYLOAD_TOO_LARGE, "The maximum amount of files exceeded.");
160+
}
161+
162+
getCourse().getRepository().uploadBatch(uploadPath, files);
163+
log.info("User {} has uploaded files to appointment entry {}.", user, getId());
164+
}
165+
166+
public boolean deleteAssignment(long user, String @NotNull ... files)
167+
{
168+
String uploadPath = uploadPath(user);
169+
File[] toBeDeleted = new File[files.length];
170+
171+
for (int i = 0; i < files.length; i++) {toBeDeleted[i] = loadFileSave(uploadPath, files[i]);}
172+
173+
boolean allDeleted = true;
174+
for (File file : toBeDeleted)
175+
{
176+
if (!file.delete())
177+
{
178+
allDeleted = false; // If any file fails to delete, mark as false
179+
}
180+
}
181+
log.info("User {} has deleted files from appointment entry {}.", user, getId());
182+
183+
return allDeleted;
184+
}
185+
186+
private @NotNull String getUploadPath(long user)
187+
{
188+
FileEntity repository = getCourse().getRepository();
189+
return repository.getFilePath(uploadPath(user));
120190
}
121191

122192
public boolean hasSubmitted(@NotNull UserEntity user)
@@ -149,16 +219,12 @@ public boolean hasSubmitted(@NotNull UserEntity user)
149219
getRoom().map(RoomEntity::toModel).orElse(null),
150220
this.getDuration().toMillis(),
151221
this.getDescription(),
152-
assignment
153-
);
222+
assignment);
154223
}
155224

156-
@Contract(pure = true, value = "-> new")
157-
@Override public String toString()
225+
@Contract(pure = true, value = "-> new") @Override public String toString()
158226
{ // Automatically generated by IntelliJ
159-
return "AppointmentEntryEntity{" +
160-
"id=" + id +
161-
'}';
227+
return "AppointmentEntryEntity{" + "id=" + id + '}';
162228
}
163229

164230
@Override public boolean equals(Object o)
@@ -285,14 +351,14 @@ protected long getTimeStamp()
285351
* without a null check.
286352
* Example usage:
287353
* <pre>
288-
* {@code
289-
* Optional<AssignmentModel> assignment = entity.getAssignment();
290-
* assignment.ifPresent(a -> {
291-
* System.out.println("Assignment Description: " + a.getDescription());
292-
* System.out.println("Submission Deadline: " + a.getSubmitUntil());
293-
* });
294-
* }
295-
* </pre>
354+
* {@code
355+
* Optional<AssignmentModel> assignment = entity.getAssignment();
356+
* assignment.ifPresent(a -> {
357+
* System.out.println("Assignment Description: " + a.getDescription());
358+
* System.out.println("Submission Deadline: " + a.getSubmitUntil());
359+
* });
360+
* }
361+
* </pre>
296362
*/
297363
public @NotNull Optional<AssignmentModel> getAssignment()
298364
{

EEDU-Frontend/src/app/user/courses/appointment/appointment.service.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,30 @@ export class AppointmentService {
4141
}
4242

4343
public fetchInsights(appointment: bigint): Observable<AssignmentInsightModel[]> {
44-
const url: string = `${this.BACKEND_URL}/submit/assignment/${appointment}/status/all`;
44+
const url: string = `${this.BACKEND_URL}/assignment/${appointment}/status/all`;
4545
return this.http.get<GenericAssignmentInsightModel[]>(url, {withCredentials: true}).pipe(map((response: GenericAssignmentInsightModel[]): AssignmentInsightModel[] => response.map((item: GenericAssignmentInsightModel): AssignmentInsightModel => AssignmentInsightModel.fromObject(item))));
4646
}
4747

4848
public fetchInsight(appointment: bigint): Observable<AssignmentInsightModel> {
49-
const url: string = `${this.BACKEND_URL}/submit/assignment/${appointment}/status`;
49+
const url: string = `${this.BACKEND_URL}/assignment/${appointment}/status`;
5050
return this.http.get<GenericAssignmentInsightModel>(url, {withCredentials: true}).pipe(map((response: GenericAssignmentInsightModel): AssignmentInsightModel => AssignmentInsightModel.fromObject(response)));
5151
}
5252

5353
public fetchUsersInsight(appointment: bigint, user: bigint): Observable<AssignmentInsightModel> {
54-
const url: string = `${this.BACKEND_URL}/submit/assignment/${appointment}/status/${user}`;
54+
const url: string = `${this.BACKEND_URL}/assignment/${appointment}/status/${user}`;
5555
return this.http.get<GenericAssignmentInsightModel>(url, {withCredentials: true}).pipe(map((response: GenericAssignmentInsightModel): AssignmentInsightModel => AssignmentInsightModel.fromObject(response)));
5656
}
5757

5858
public submitAssignment(appointment: bigint, assignmentFiles: File[]): Observable<HttpEvent<any>> {
59-
const url: string = `${this.BACKEND_URL}/submit/assignment/${appointment}`;
59+
const url: string = `${this.BACKEND_URL}/assignment/${appointment}/submit`;
6060
return this._fileService.uploadFiles(url, assignmentFiles);
6161
}
6262

63+
public deleteAssignment(appointment: bigint, assignmentFiles: string[]): Observable<void> {
64+
const url: string = `${this.BACKEND_URL}/assignment/${appointment}/delete/${assignmentFiles.toString()}`;
65+
return this.http.delete<void>(url, {withCredentials: true});
66+
}
67+
6368
/**
6469
* Creates appointments for a specified course.
6570
*

0 commit comments

Comments
 (0)