Skip to content

Commit 84f4944

Browse files
Development: Add new server endpoints for attendance checker app (#11419)
1 parent c0bc19d commit 84f4944

18 files changed

+582
-42
lines changed

src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
import de.tum.cit.aet.artemis.core.repository.UserRepository;
5757
import de.tum.cit.aet.artemis.core.security.Role;
5858
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor;
59-
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor;
6059
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent;
6160
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor;
6261
import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse;
@@ -407,13 +406,13 @@ public ResponseEntity<byte[]> getCourseCodeOfConduct() throws IOException {
407406
* @return The requested file, 403 if the logged-in user is not allowed to access it, or 404 if the file doesn't exist
408407
*/
409408
@GetMapping("files/exam-user/signatures/{examUserId}/*")
410-
@EnforceAtLeastInstructor
409+
@EnforceAtLeastTutor
411410
public ResponseEntity<byte[]> getUserSignature(@PathVariable Long examUserId) {
412411
log.debug("REST request to get signature for exam user : {}", examUserId);
413412
ExamUserApi api = examUserApi.orElseThrow(() -> new ExamApiNotPresentException(ExamUserApi.class));
414413

415414
ExamUser examUser = api.findWithExamById(examUserId).orElseThrow();
416-
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, examUser.getExam().getCourse(), null);
415+
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.TEACHING_ASSISTANT, examUser.getExam().getCourse(), null);
417416

418417
return buildFileResponse(getActualPathFromPublicPathString(examUser.getSigningImagePath(), FilePathType.EXAM_USER_SIGNATURE), false);
419418
}
@@ -425,13 +424,13 @@ public ResponseEntity<byte[]> getUserSignature(@PathVariable Long examUserId) {
425424
* @return The requested file, 403 if the logged-in user is not allowed to access it, or 404 if the file doesn't exist
426425
*/
427426
@GetMapping("files/exam-user/{examUserId}/*")
428-
@EnforceAtLeastInstructor
427+
@EnforceAtLeastTutor
429428
public ResponseEntity<byte[]> getExamUserImage(@PathVariable long examUserId) {
430429
log.debug("REST request to get image for exam user : {}", examUserId);
431430
ExamUserApi api = examUserApi.orElseThrow(() -> new ExamApiNotPresentException(ExamUserApi.class));
432431

433432
ExamUser examUser = api.findWithExamById(examUserId).orElseThrow();
434-
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, examUser.getExam().getCourse(), null);
433+
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.TEACHING_ASSISTANT, examUser.getExam().getCourse(), null);
435434

436435
return buildFileResponse(getActualPathFromPublicPathString(examUser.getStudentImagePath(), FilePathType.EXAM_USER_IMAGE), true);
437436
}

src/main/java/de/tum/cit/aet/artemis/exam/domain/ExamUser.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
import jakarta.persistence.JoinColumn;
66
import jakarta.persistence.ManyToOne;
77
import jakarta.persistence.Table;
8+
import jakarta.persistence.Transient;
89
import jakarta.validation.constraints.Size;
910

11+
import com.fasterxml.jackson.annotation.JsonIgnore;
1012
import com.fasterxml.jackson.annotation.JsonInclude;
1113

1214
import de.tum.cit.aet.artemis.core.domain.AbstractAuditingEntity;
1315
import de.tum.cit.aet.artemis.core.domain.User;
16+
import de.tum.cit.aet.artemis.exam.domain.room.ExamRoom;
17+
import de.tum.cit.aet.artemis.exam.dto.room.ExamSeatDTO;
1418

1519
@Entity
1620
@Table(name = "exam_user")
@@ -29,6 +33,22 @@ public class ExamUser extends AbstractAuditingEntity {
2933
@Column(name = "planned_seat")
3034
private String plannedSeat;
3135

36+
@JsonIgnore
37+
@Transient
38+
private ExamRoom plannedRoomTransient;
39+
40+
@JsonIgnore
41+
@Transient
42+
private ExamSeatDTO plannedSeatTransient;
43+
44+
@JsonIgnore
45+
@Transient
46+
private ExamRoom actualRoomTransient;
47+
48+
@JsonIgnore
49+
@Transient
50+
private ExamSeatDTO actualSeatTransient;
51+
3252
@Column(name = "did_check_image")
3353
private boolean didCheckImage = false;
3454

@@ -89,6 +109,32 @@ public void setPlannedSeat(String plannedSeat) {
89109
this.plannedSeat = plannedSeat;
90110
}
91111

112+
public ExamRoom getPlannedRoomTransient() {
113+
return plannedRoomTransient;
114+
}
115+
116+
public ExamSeatDTO getPlannedSeatTransient() {
117+
return plannedSeatTransient;
118+
}
119+
120+
public void setTransientPlannedRoomAndSeat(ExamRoom plannedRoom, ExamSeatDTO plannedSeat) {
121+
this.plannedRoomTransient = plannedRoom;
122+
this.plannedSeatTransient = plannedSeat;
123+
}
124+
125+
public ExamRoom getActualRoomTransient() {
126+
return actualRoomTransient;
127+
}
128+
129+
public ExamSeatDTO getActualSeatTransient() {
130+
return actualSeatTransient;
131+
}
132+
133+
public void setTransientActualRoomAndSeat(ExamRoom actualRoom, ExamSeatDTO actualSeat) {
134+
this.actualRoomTransient = actualRoom;
135+
this.actualSeatTransient = actualSeat;
136+
}
137+
92138
public boolean getDidCheckRegistrationNumber() {
93139
return didCheckRegistrationNumber;
94140
}

src/main/java/de/tum/cit/aet/artemis/exam/dto/ExamUserDTO.java

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,54 @@
33
import jakarta.validation.constraints.Email;
44
import jakarta.validation.constraints.Size;
55

6+
import org.jspecify.annotations.Nullable;
7+
68
import com.fasterxml.jackson.annotation.JsonInclude;
79

10+
import de.tum.cit.aet.artemis.core.domain.User;
11+
812
/**
913
* Contains the information about an exam user
1014
*/
1115
@JsonInclude(JsonInclude.Include.NON_EMPTY)
12-
public record ExamUserDTO(@Size(max = 50) String login, @Size(max = 50) String firstName, @Size(max = 50) String lastName, @Size(max = 10) String registrationNumber,
13-
@Email @Size(max = 100) String email, String studentIdentifier, String room, String seat, boolean didCheckImage, boolean didCheckName, boolean didCheckRegistrationNumber,
14-
boolean didCheckLogin, @Size(max = 100) String signingImagePath) {
16+
// @formatter:off
17+
public record ExamUserDTO(
18+
@Size(max = 50) String login,
19+
@Size(max = 50) String firstName,
20+
@Size(max = 50) String lastName,
21+
@Size(max = 10) String registrationNumber,
22+
@Email @Size(max = 100) String email,
23+
String studentIdentifier,
24+
String room,
25+
String seat,
26+
boolean didCheckImage,
27+
boolean didCheckName,
28+
boolean didCheckRegistrationNumber,
29+
boolean didCheckLogin,
30+
@Size(max = 100) String signingImagePath,
31+
// TODO: Remove everything below here when no longer supporting Exam Checker app version 2.0
32+
@Nullable Long id,
33+
@Nullable String actualRoom,
34+
@Nullable String actualSeat,
35+
@Nullable String plannedRoom,
36+
@Nullable String plannedSeat,
37+
@Nullable String studentImagePath,
38+
@Nullable ExamUserDetailsDTO user
39+
) {
40+
// TODO: Remove this DTO when no longer supporting Exam Checker app version 2.0
41+
/**
42+
* Contains the details about an exam user
43+
*/
44+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
45+
public record ExamUserDetailsDTO(
46+
@Size(max = 50) String login,
47+
@Size(max = 50) String name,
48+
@Nullable @Size(max = 10) String visibleRegistrationNumber,
49+
Long id
50+
) {
51+
public static ExamUserDetailsDTO fromUser(User user) {
52+
return new ExamUserDetailsDTO(user.getLogin(), user.getName(), user.getVisibleRegistrationNumber(), user.getId());
53+
}
54+
}
1555
}
56+
// @formatter:on
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package de.tum.cit.aet.artemis.exam.dto.room;
2+
3+
import java.time.ZonedDateTime;
4+
import java.util.List;
5+
import java.util.Set;
6+
import java.util.stream.Collectors;
7+
8+
import jakarta.validation.constraints.Email;
9+
import jakarta.validation.constraints.NotBlank;
10+
import jakarta.validation.constraints.NotEmpty;
11+
import jakarta.validation.constraints.NotNull;
12+
import jakarta.validation.constraints.Size;
13+
14+
import org.jspecify.annotations.Nullable;
15+
import org.springframework.util.StringUtils;
16+
17+
import com.fasterxml.jackson.annotation.JsonInclude;
18+
19+
import de.tum.cit.aet.artemis.exam.domain.Exam;
20+
import de.tum.cit.aet.artemis.exam.domain.ExamUser;
21+
import de.tum.cit.aet.artemis.exam.domain.room.ExamRoom;
22+
23+
// @formatter:off
24+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
25+
record ExamRoomForAttendanceCheckerDTO(
26+
long id,
27+
@NotBlank String roomNumber,
28+
@Nullable String alternativeRoomNumber,
29+
@NotBlank String name,
30+
@Nullable String alternativeName,
31+
@NotBlank String building,
32+
@NotNull List<ExamSeatDTO> seats
33+
) {
34+
static ExamRoomForAttendanceCheckerDTO from(ExamRoom examRoom) {
35+
return new ExamRoomForAttendanceCheckerDTO(
36+
examRoom.getId(),
37+
examRoom.getRoomNumber(),
38+
examRoom.getAlternativeRoomNumber(),
39+
examRoom.getName(),
40+
examRoom.getAlternativeName(),
41+
examRoom.getBuilding(),
42+
examRoom.getSeats()
43+
);
44+
}
45+
}
46+
47+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
48+
record ExamUserLocationDTO(
49+
@Nullable Long roomId, // null if legacy version
50+
@NotBlank String roomNumber, // examUser.plannedRoom if legacy version
51+
@NotBlank String seatName // examUser.plannedSeat if legacy version
52+
) {
53+
static ExamUserLocationDTO plannedFrom(ExamUser examUser) {
54+
final boolean isLegacy = examUser.getPlannedRoomTransient() == null || examUser.getPlannedSeatTransient() == null;
55+
56+
return new ExamUserLocationDTO(
57+
isLegacy ? null : examUser.getPlannedRoomTransient().getId(),
58+
isLegacy ? examUser.getPlannedRoom() : examUser.getPlannedRoomTransient().getRoomNumber(),
59+
isLegacy ? examUser.getPlannedSeat() : examUser.getPlannedSeatTransient().name()
60+
);
61+
}
62+
63+
static ExamUserLocationDTO actualFrom(ExamUser examUser) {
64+
if (examUser.getActualRoom() == null || examUser.getActualSeat() == null) {
65+
// examUser has not been moved
66+
return null;
67+
}
68+
69+
final boolean useLegacyFields = examUser.getActualRoomTransient() == null || examUser.getActualSeatTransient() == null;
70+
71+
return new ExamUserLocationDTO(
72+
useLegacyFields ? null : examUser.getActualRoomTransient().getId(),
73+
useLegacyFields ? examUser.getActualRoom() : examUser.getActualRoomTransient().getRoomNumber(),
74+
useLegacyFields ? examUser.getActualSeat() : examUser.getActualSeatTransient().name()
75+
);
76+
}
77+
}
78+
79+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
80+
record ExamUserWithExamRoomAndSeatDTO (
81+
@NotBlank @Size(max = 50) String login,
82+
// Names are nullable because not everyone has a first and/or last name
83+
@Nullable @Size(max = 50) String firstName,
84+
@Nullable @Size(max = 50) String lastName,
85+
@NotBlank @Size(max = 10) String registrationNumber,
86+
@Nullable @Email @Size(max = 100) String email,
87+
@Nullable String imageUrl,
88+
boolean didCheckImage,
89+
boolean didCheckName,
90+
boolean didCheckRegistrationNumber,
91+
boolean didCheckLogin,
92+
@Nullable String signingImagePath,
93+
@NotNull ExamUserLocationDTO plannedLocation,
94+
@Nullable ExamUserLocationDTO actualLocation
95+
) {
96+
static ExamUserWithExamRoomAndSeatDTO from(ExamUser examUser) {
97+
return new ExamUserWithExamRoomAndSeatDTO(
98+
examUser.getUser().getLogin(),
99+
examUser.getUser().getFirstName(),
100+
examUser.getUser().getLastName(),
101+
examUser.getUser().getRegistrationNumber(),
102+
examUser.getUser().getEmail(),
103+
examUser.getStudentImagePath(),
104+
examUser.getDidCheckImage(),
105+
examUser.getDidCheckName(),
106+
examUser.getDidCheckRegistrationNumber(),
107+
examUser.getDidCheckLogin(),
108+
examUser.getSigningImagePath(),
109+
ExamUserLocationDTO.plannedFrom(examUser),
110+
ExamUserLocationDTO.actualFrom(examUser)
111+
);
112+
}
113+
}
114+
115+
/**
116+
* DTO containing all relevant information for the attendance checker app.
117+
* This DTO and all its children are exclusively sent from the server to the client.
118+
*/
119+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
120+
public record AttendanceCheckerAppExamInformationDTO(
121+
long examId,
122+
@NotBlank String examTitle,
123+
@NotNull ZonedDateTime startDate,
124+
@NotNull ZonedDateTime endDate,
125+
boolean isTestExam,
126+
long courseId,
127+
@NotBlank String courseTitle,
128+
@NotNull Set<ExamRoomForAttendanceCheckerDTO> examRoomsUsedInExam, // empty if legacy version
129+
@NotEmpty Set<ExamUserWithExamRoomAndSeatDTO> examUsersWithExamRoomAndSeat
130+
) {
131+
/**
132+
* Create an AttendanceCheckerAppExamInformationDTO from the given exam and its rooms
133+
*
134+
* @param exam the exam
135+
* @param examRooms the rooms used in the exam
136+
* @return information for the attendance checker app
137+
*/
138+
public static AttendanceCheckerAppExamInformationDTO from(Exam exam, Set<ExamRoom> examRooms) {
139+
Set<ExamUser> examUsersWhoHaveBeenDistributed = exam.getExamUsers().stream()
140+
.filter(examUser -> StringUtils.hasText(examUser.getPlannedRoom()) && StringUtils.hasText(examUser.getPlannedSeat()))
141+
.collect(Collectors.toSet());
142+
143+
return new AttendanceCheckerAppExamInformationDTO(
144+
exam.getId(),
145+
exam.getTitle(),
146+
exam.getStartDate(),
147+
exam.getEndDate(),
148+
exam.isTestExam(),
149+
exam.getCourse().getId(),
150+
exam.getCourse().getTitle(),
151+
examRooms.stream().map(ExamRoomForAttendanceCheckerDTO::from).collect(Collectors.toSet()),
152+
examUsersWhoHaveBeenDistributed.stream().map(ExamUserWithExamRoomAndSeatDTO::from).collect(Collectors.toSet())
153+
);
154+
}
155+
}
156+
// @formatter:on

src/main/java/de/tum/cit/aet/artemis/exam/repository/ExamRepository.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public interface ExamRepository extends ArtemisJpaRepository<Exam, Long> {
9393
List<Exam> findAllByEndDateGreaterThanEqual(@Param("date") ZonedDateTime date);
9494

9595
/**
96-
* Query which fetches all the active exams for which the user is instructor.
96+
* Query which fetches all the active exams for which the user is at least teaching assistant.
9797
*
9898
* @param groups user groups
9999
* @param pageable Pageable
@@ -104,11 +104,13 @@ public interface ExamRepository extends ArtemisJpaRepository<Exam, Long> {
104104
@Query("""
105105
SELECT e
106106
FROM Exam e
107-
WHERE e.course.instructorGroupName IN :groups
107+
WHERE (e.course.instructorGroupName IN :groups
108+
OR e.course.editorGroupName IN :groups
109+
OR e.course.teachingAssistantGroupName IN :groups)
108110
AND e.visibleDate >= :fromDate
109111
AND e.visibleDate <= :toDate
110112
""")
111-
Page<Exam> findAllActiveExamsInCoursesWhereInstructor(@Param("groups") Set<String> groups, Pageable pageable, @Param("fromDate") ZonedDateTime fromDate,
113+
Page<Exam> findAllActiveExamsInCoursesWhereAtLeastTutor(@Param("groups") Set<String> groups, Pageable pageable, @Param("fromDate") ZonedDateTime fromDate,
112114
@Param("toDate") ZonedDateTime toDate);
113115

114116
/**

src/main/java/de/tum/cit/aet/artemis/exam/repository/ExamRoomRepository.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.springframework.context.annotation.Lazy;
77
import org.springframework.data.jpa.repository.EntityGraph;
88
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
910
import org.springframework.stereotype.Repository;
1011

1112
import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;
@@ -136,4 +137,13 @@ SELECT er.id AS id, er.roomNumber AS roomNumber, er.alternativeRoomNumber AS alt
136137
WHERE roomPartition.rowNumber = 1
137138
""")
138139
Set<ExamRoomForDistributionDTO> findAllCurrentExamRoomsForDistribution();
140+
141+
@Query("""
142+
SELECT er
143+
FROM ExamRoom er
144+
JOIN ExamRoomExamAssignment erea
145+
ON er.id = erea.examRoom.id
146+
WHERE erea.exam.id = :examId
147+
""")
148+
Set<ExamRoom> findAllByExamId(@Param("examId") long examId);
139149
}

0 commit comments

Comments
 (0)