Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exam mode: Enable students to participate in the test exam multiple times #8609

Open
wants to merge 120 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 114 commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
7392a09
refactor StudentExamService
coolchock May 13, 2024
89ef7fb
refactor ParticipationService^
coolchock May 13, 2024
61cba39
add student_exam_participation table
coolchock May 13, 2024
8da735c
assign generated participations to the student exam
coolchock May 14, 2024
07c541e
refactor start exercise method
coolchock May 14, 2024
c6ced67
remove index to allow multiple participations with the same student_i…
coolchock May 14, 2024
fff4bfa
refactor exam access service
coolchock May 14, 2024
6dc3c42
add participation_id column to participation table
coolchock May 16, 2024
4b90546
add participation_id column to the index
coolchock May 16, 2024
b882350
add a query to fetch last text exam participation
coolchock May 16, 2024
ffb9eab
add changelogs to master.xml
coolchock May 16, 2024
6912659
change navigation in the attempt-review-component
coolchock May 16, 2024
261c3be
refactor ExamSubmissionService
coolchock May 16, 2024
43b0ef1
refactor ExamDateService
coolchock May 16, 2024
c055ef1
add isFinished method in the StudentExam class
coolchock May 16, 2024
9e2e08c
change column name from participation_id to number_of_attempts
coolchock May 16, 2024
dd4e8a7
add query to check if an exam is a test exam
coolchock May 16, 2024
e552578
add column to Participation entity
coolchock May 16, 2024
1ab81f1
set number of attempts
coolchock May 16, 2024
bf79e1b
check for the testExam in the live-events
coolchock May 16, 2024
7f3da78
change method in the exam summary method
coolchock May 16, 2024
f632053
filter out participations in the ExamService
coolchock May 16, 2024
b9cd7db
set number of attempts to 255
coolchock May 16, 2024
3a022b5
add comment
coolchock May 16, 2024
0023448
adjust comments
coolchock May 16, 2024
0fb3cfd
remove commented method
coolchock May 16, 2024
c58dd87
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock May 27, 2024
7d4a49c
fetch latest student exam in ResultService
coolchock May 28, 2024
0fbb9c3
add repository method to fetch latest StudentParticipation
coolchock May 28, 2024
1a8c1a5
distinguish between test exam and other exam types in ProgrammingExer…
coolchock May 28, 2024
7fc8758
change ParticipationServiceTest
coolchock May 28, 2024
aaaa4b2
fetch latest participation for the test exam exercise
coolchock May 29, 2024
87b9321
implement findLatest methods
coolchock May 29, 2024
38e08bf
remove attemptNumber from repository slug
coolchock Jun 3, 2024
cb24abf
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 3, 2024
b1c1b9c
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 5, 2024
dc95707
remove redundant call to set attempt number
coolchock Jun 5, 2024
564bde9
convert list to a set to improve performance
coolchock Jun 5, 2024
ca54780
add attempt number to repository name
coolchock Jun 5, 2024
da998a0
rename method and remove submission policy
coolchock Jun 7, 2024
044fadc
change router link
coolchock Jun 7, 2024
7e7b868
change logic of openStudentExam
coolchock Jun 7, 2024
ae91e10
make attempt component non-clickable if the attempt was not submitted…
coolchock Jun 7, 2024
5f34573
change repositoryLink for test-exams
coolchock Jun 7, 2024
49f5d11
remove @Transactional
coolchock Jun 7, 2024
1e1c3b5
add @Param annotations
coolchock Jun 7, 2024
ed10000
optimize query by using limit and order instead of select max
coolchock Jun 7, 2024
b604bf0
use toList() instead of Collectors
coolchock Jun 7, 2024
52f1e5a
change repository method
coolchock Jun 7, 2024
74599b9
fix the issue, that submissions of first attempt are not saved
coolchock Jun 9, 2024
1c0c7af
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 10, 2024
e4ddb76
distinguish between test exams in repository method
coolchock Jun 10, 2024
cd7dc52
move filtering of the student exam participations to the repository m…
coolchock Jun 10, 2024
631dd2f
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 12, 2024
8476dcd
fix tests
coolchock Jun 12, 2024
d00108f
adjust architecture test
coolchock Jun 12, 2024
71171c0
avoid unnecessary DB call
coolchock Jun 12, 2024
6c085c3
adjust repository methods
coolchock Jun 12, 2024
8c9f604
rename method^
coolchock Jun 12, 2024
6500a9e
fix test
coolchock Jun 12, 2024
3b9a656
remove unnecessary test
coolchock Jun 12, 2024
8fad453
adjust translations
coolchock Jun 14, 2024
ac396fd
refactor getExamInCourseElseThrow method
coolchock Jun 14, 2024
2e4ad8c
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 14, 2024
0a2519c
translations
coolchock Jun 15, 2024
8ed669d
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 15, 2024
6b4856e
resolve rabbit comments
coolchock Jun 16, 2024
c6d851e
adjust tests
coolchock Jun 16, 2024
ea1f359
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 16, 2024
b186bae
adjust filtering of sensitive feedbacks in exam exercises
coolchock Jun 17, 2024
afeed60
implement isTestExamExercise method
coolchock Jun 17, 2024
7b8ec72
isTestExam method
coolchock Jun 17, 2024
ee30325
add numberOfAttempts @param docu
coolchock Jun 17, 2024
bfd9323
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 21, 2024
bac9202
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 23, 2024
b40c3ab
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Jun 24, 2024
c85c599
fix instructors not being able to participate
coolchock Jun 24, 2024
7644eb6
prettier
coolchock Jun 24, 2024
54853d5
remove LIMIT from JPQL queries
coolchock Jun 24, 2024
edc9c18
add ElseThrow to method signature^
coolchock Jun 24, 2024
f913762
Remove unused files in new exam mode ui
edkaya Jul 10, 2024
ebcdd54
Resolve merge conflict
edkaya Jul 10, 2024
2025853
Implement new attempt design and integrate to new exam mode ui
edkaya Jul 10, 2024
8870714
Fix one client test
edkaya Jul 10, 2024
1b6c16e
Add javadoc comments
edkaya Jul 10, 2024
588e161
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Aug 20, 2024
dc78eec
solve problems after merging develop
coolchock Aug 21, 2024
613fd3a
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Aug 27, 2024
32e4e75
add test exam to condition
coolchock Aug 27, 2024
6e00298
modify repository method to find first participation with submissions
coolchock Aug 27, 2024
183d28c
merge changes in sidebar-card-item.component.html
coolchock Aug 28, 2024
1398574
reload attempts after finishing one
coolchock Aug 28, 2024
35d6501
rename number_of_attempts to attempts in migrations and class
coolchock Aug 28, 2024
3fa1a04
remove entity graph
coolchock Aug 28, 2024
8ec09d1
fix architecture test
coolchock Sep 1, 2024
72f8906
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Sep 1, 2024
f0730d6
add tests
coolchock Sep 1, 2024
42258ed
resolve TODOs
coolchock Sep 1, 2024
73acd0b
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Sep 3, 2024
622ac46
don't show percentDiff for test exams
coolchock Sep 3, 2024
53c2219
show start and end dates for test exams
coolchock Sep 3, 2024
11abf89
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Sep 15, 2024
f8c24cb
resolve merge conflicts
coolchock Sep 15, 2024
7956b02
incorporate feedback
coolchock Sep 17, 2024
690d249
add javadoc
coolchock Sep 17, 2024
d75d88f
improve coverage
coolchock Sep 17, 2024
def8e4e
fix tests
coolchock Sep 17, 2024
a51b76e
improve coverage
coolchock Sep 17, 2024
5d7067d
fix typo
coolchock Sep 17, 2024
0bed9b0
use findFirst to simplify the code
coolchock Sep 21, 2024
2bd0b8e
use template literals to improve readability
coolchock Sep 21, 2024
1d11448
rename method
coolchock Sep 21, 2024
abea29b
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Sep 21, 2024
6c526d8
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Oct 1, 2024
917afe7
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Oct 15, 2024
dcc5903
resolve merge conflicts
coolchock Oct 15, 2024
0658e9a
resolve merge conflicts
coolchock Oct 15, 2024
0400798
resolve merge conflicts
coolchock Oct 16, 2024
e83d11e
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Oct 20, 2024
6fd8939
Merge branch 'refs/heads/develop' into feature/exam-mode/participate-…
coolchock Nov 10, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,8 @@ private void filterSensitiveFeedbackInCourseExercise(Participation participation
private void filterSensitiveFeedbacksInExamExercise(Participation participation, Collection<Result> results, Exercise exercise) {
Exam exam = exercise.getExerciseGroup().getExam();
boolean shouldResultsBePublished = exam.resultsPublished();
if (!shouldResultsBePublished && exam.isTestExam() && participation instanceof StudentParticipation studentParticipation) {
var participant = studentParticipation.getParticipant();
var studentExamOptional = studentExamRepository.findByExamIdAndUserId(exam.getId(), participant.getId());
if (!shouldResultsBePublished && exam.isTestExam() && participation instanceof StudentParticipation) {
var studentExamOptional = studentExamRepository.findByExamIdAndParticipationId(exam.getId(), participation.getId());
if (studentExamOptional.isPresent()) {
shouldResultsBePublished = studentExamOptional.get().areResultsPublishedYet();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import de.tum.cit.aet.artemis.core.domain.AbstractAuditingEntity;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.quiz.domain.QuizQuestion;

@Entity
Expand Down Expand Up @@ -84,6 +85,12 @@ public class StudentExam extends AbstractAuditingEntity {
@JoinTable(name = "student_exam_quiz_question", joinColumns = @JoinColumn(name = "student_exam_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "quiz_question_id", referencedColumnName = "id"))
private List<QuizQuestion> quizQuestions = new ArrayList<>();

@ManyToMany
@JoinTable(name = "student_exam_participation", joinColumns = @JoinColumn(name = "student_exam_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "participation_id", referencedColumnName = "id"))
@OrderColumn(name = "participation_order")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
private List<StudentParticipation> studentParticipations = new ArrayList<>();

public Boolean isSubmitted() {
return submitted;
}
Expand Down Expand Up @@ -199,6 +206,14 @@ public void setQuizQuestions(List<QuizQuestion> quizQuestions) {
this.quizQuestions = quizQuestions;
}

public List<StudentParticipation> getStudentParticipations() {
return studentParticipations;
}

public void setStudentParticipations(List<StudentParticipation> studentParticipations) {
this.studentParticipations = studentParticipations;
}

/**
* Adds the given exam session to the student exam
*
Expand Down Expand Up @@ -231,6 +246,16 @@ public Boolean isEnded() {
return ZonedDateTime.now().isAfter(getIndividualEndDate());
}

/**
* Check if the individual student exam is finished
* A student exam is finished if it's started and either submitted or the time has passed
*
* @return true if the exam is finished, otherwise false
*/
public boolean isFinished() {
return Boolean.TRUE.equals(this.isStarted()) && (Boolean.TRUE.equals(this.isEnded()) || Boolean.TRUE.equals(this.isSubmitted()));
}

/**
* Returns the individual exam end date taking the working time of this student exam into account.
* For test exams, the startedDate needs to be defined as this is not equal to exam.startDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,4 +508,5 @@ private static Map<Long, Integer> convertListOfCountsIntoMap(List<long[]> examId
AND registeredUsers.user.id = :userId
""")
Set<Exam> findActiveExams(@Param("courseIds") Set<Long> courseIds, @Param("userId") long userId, @Param("visible") ZonedDateTime visible, @Param("end") ZonedDateTime end);

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,18 @@ public interface StudentExamRepository extends ArtemisJpaRepository<StudentExam,
@EntityGraph(type = LOAD, attributePaths = { "exercises" })
Optional<StudentExam> findWithExercisesById(Long studentExamId);

@EntityGraph(type = LOAD, attributePaths = { "exercises", "studentParticipations" })
Optional<StudentExam> findWithExercisesAndStudentParticipationsById(Long studentExamId);

coolchock marked this conversation as resolved.
Show resolved Hide resolved
@Query("""
SELECT se
FROM StudentExam se
LEFT JOIN FETCH se.exercises e
LEFT JOIN FETCH e.submissionPolicy
LEFT JOIN FETCH se.examSessions
LEFT JOIN FETCH se.studentParticipations
WHERE se.id = :studentExamId
""")
Optional<StudentExam> findWithExercisesSubmissionPolicyAndSessionsById(@Param("studentExamId") long studentExamId);
Optional<StudentExam> findWithExercisesAndSessionsAndStudentParticipationsById(@Param("studentExamId") long studentExamId);

@Query("""
SELECT DISTINCT se
Expand Down Expand Up @@ -190,6 +193,29 @@ SELECT COUNT(se)
""")
Optional<StudentExam> findByExamIdAndUserId(@Param("examId") long examId, @Param("userId") long userId);

Optional<StudentExam> findFirstByExamIdAndUserIdOrderByIdDesc(long examId, long userId);

@Query("""
SELECT se
FROM StudentExam se
JOIN se.studentParticipations p
WHERE se.exam.id = :examId
AND p.id = :participationId
""")
Optional<StudentExam> findByExamIdAndParticipationId(@Param("examId") long examId, @Param("participationId") long participationId);

/**
* Return the StudentExam for the given examId and userId, if possible. For test exams, the latest Student Exam is returned.
*
* @param examId id of the exam
* @param userId id of the user
* @return the student exam
* @throws EntityNotFoundException if no student exams could be found
*/
default StudentExam findOneByExamIdAndUserIdElseThrow(long examId, long userId) {
return getValueElseThrow(this.findFirstByExamIdAndUserIdOrderByIdDesc(examId, userId));
}

/**
* Checks if any StudentExam exists for the given user (student) id in the given course.
*
Expand Down Expand Up @@ -253,7 +279,17 @@ SELECT MAX(se.workingTime)
AND se.exam.testExam = TRUE
AND se.testRun = FALSE
""")
List<StudentExam> findStudentExamForTestExamsByUserIdAndCourseId(@Param("userId") Long userId, @Param("courseId") Long courseId);
List<StudentExam> findStudentExamsForTestExamsByUserIdAndCourseId(@Param("userId") Long userId, @Param("courseId") Long courseId);

coolchock marked this conversation as resolved.
Show resolved Hide resolved
@Query("""
SELECT DISTINCT se
FROM StudentExam se
WHERE se.user.id = :userId
AND se.exam.id = :examId
AND se.exam.testExam = TRUE
AND se.testRun = FALSE
""")
List<StudentExam> findStudentExamsForTestExamsByUserIdAndExamId(@Param("userId") Long userId, @Param("examId") Long examId);
coolchock marked this conversation as resolved.
Show resolved Hide resolved

@Query("""
SELECT DISTINCT se
Expand Down Expand Up @@ -313,15 +349,20 @@ default StudentExam findByIdWithExercisesElseThrow(Long studentExamId) {
return getValueElseThrow(findWithExercisesById(studentExamId), studentExamId);
}

@NotNull
default StudentExam findByIdWithExercisesAndStudentParticipationsElseThrow(Long studentExamId) {
return getValueElseThrow(findWithExercisesAndStudentParticipationsById(studentExamId));
}
coolchock marked this conversation as resolved.
Show resolved Hide resolved

/**
* Get one student exam by id with exercises, programming exercise submission policy and sessions
* Get one student exam by id with exercises, sessions and student participations
*
* @param studentExamId the id of the student exam
* @return the student exam with exercises
* @return the student exam with exercises, sessions and student participations
*/
@NotNull
default StudentExam findByIdWithExercisesSubmissionPolicyAndSessionsElseThrow(Long studentExamId) {
return getValueElseThrow(findWithExercisesSubmissionPolicyAndSessionsById(studentExamId), studentExamId);
default StudentExam findByIdWithExercisesAndSessionsAndStudentParticipationsElseThrow(Long studentExamId) {
return getValueElseThrow(findWithExercisesAndSessionsAndStudentParticipationsById(studentExamId), studentExamId);
coolchock marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;

import org.springframework.context.annotation.Profile;
Expand Down Expand Up @@ -48,15 +49,19 @@ public class ExamAccessService {

private final StudentExamService studentExamService;

private final ExamDateService examDateService;

public ExamAccessService(ExamRepository examRepository, StudentExamRepository studentExamRepository, AuthorizationCheckService authorizationCheckService,
UserRepository userRepository, CourseRepository courseRepository, ExamRegistrationService examRegistrationService, StudentExamService studentExamService) {
UserRepository userRepository, CourseRepository courseRepository, ExamRegistrationService examRegistrationService, StudentExamService studentExamService,
ExamDateService examDateService) {
this.examRepository = examRepository;
this.studentExamRepository = studentExamRepository;
this.authorizationCheckService = authorizationCheckService;
this.userRepository = userRepository;
this.courseRepository = courseRepository;
this.examRegistrationService = examRegistrationService;
this.studentExamService = studentExamService;
this.examDateService = examDateService;
}

/**
Expand All @@ -67,38 +72,14 @@ public ExamAccessService(ExamRepository examRepository, StudentExamRepository st
* @param examId The id of the exam
* @return a ResponseEntity with the exam
*/

public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) {
User currentUser = userRepository.getUserWithGroupsAndAuthorities();

// TODO: we should distinguish the whole method between test exam and real exam to improve the readability of the code
// Check that the current user is at least student in the course.
Course course = courseRepository.findByIdElseThrow(courseId);
authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, currentUser);

// Check that the student exam exists
Optional<StudentExam> optionalStudentExam = studentExamRepository.findByExamIdAndUserId(examId, currentUser.getId());

StudentExam studentExam;
// If an studentExam can be fund, we can proceed
if (optionalStudentExam.isPresent()) {
studentExam = optionalStudentExam.get();
}
else {
// Only Test Exams can be self-created by the user.
Exam examWithExerciseGroupsAndExercises = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examId);

if (!examWithExerciseGroupsAndExercises.isTestExam()) {
// We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam
throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME,
"StudentExamGenerationOnlyForTestExams", true);
}
studentExam = studentExamService.generateTestExam(examWithExerciseGroupsAndExercises, currentUser);
// For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource
studentExam.setExercises(null);
}

Exam exam = studentExam.getExam();

Exam exam = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examId);
checkExamBelongsToCourseElseThrow(courseId, exam);

if (!examId.equals(exam.getId())) {
Expand All @@ -111,14 +92,71 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) {
}

if (exam.isTestExam()) {
// Check that the current user is registered for the test exam. Otherwise, the student can self-register
examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course, exam.getId(), currentUser);
return getOrGenerateTestExam(exam, course, currentUser);
}
else {
return getNormalExam(examId, currentUser.getId());
}
// NOTE: the check examRepository.isUserRegisteredForExam is not necessary because we already checked before that there is a student exam in this case for the current user
}

/**
* Fetches an unfinished StudentExam for a test exam if one exists. If no unfinished StudentExam exists, generates a new one.
*
* @param exam The exam which StudentExam belongs to
* @param course The course which the exam belongs to
* @return the StudentExam
* @throws BadRequestAlertException If the exam had already ended
* @throws IllegalStateException If the user has more than one unfinished student exam
*/
private StudentExam getOrGenerateTestExam(Exam exam, Course course, User currentUser) {
StudentExam studentExam;

if (this.examDateService.isExamOver(exam)) {
throw new BadRequestAlertException("Test exam has already ended", ENTITY_NAME, "examHasAlreadyEnded", true);
}
coolchock marked this conversation as resolved.
Show resolved Hide resolved

List<StudentExam> unfinishedStudentExams = studentExamRepository.findStudentExamsForTestExamsByUserIdAndExamId(currentUser.getId(), exam.getId()).stream()
.filter(attempt -> !attempt.isFinished()).toList();
coolchock marked this conversation as resolved.
Show resolved Hide resolved

if (unfinishedStudentExams.isEmpty()) {
studentExam = studentExamService.generateTestExam(exam, currentUser);
// For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource
studentExam.setExercises(null);
}
else if (unfinishedStudentExams.size() == 1) {
studentExam = unfinishedStudentExams.getFirst();
coolchock marked this conversation as resolved.
Show resolved Hide resolved
}
else {
throw new IllegalStateException(
"User " + currentUser.getId() + " has " + unfinishedStudentExams.size() + " unfinished test exams for exam " + exam.getId() + " in course " + course.getId());
}
// Check that the current user is registered for the test exam. Otherwise, the student can self-register
examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course, exam.getId(), currentUser);
return studentExam;
}

/**
* Fetches a real exam for the given examId and userId.
*
* @param examId the id of the Exam
* @param userId the id of the User
* @return the StudentExam
*/
coolchock marked this conversation as resolved.
Show resolved Hide resolved
private StudentExam getNormalExam(Long examId, Long userId) {
// Check that the student exam exists
Optional<StudentExam> optionalStudentExam = studentExamRepository.findByExamIdAndUserId(examId, userId);
// If an studentExam can be found, we can proceed
if (optionalStudentExam.isPresent()) {
return optionalStudentExam.get();
}
else {
// We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam
throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME, "StudentExamGenerationOnlyForTestExams",
true);
coolchock marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* Checks if the current user is allowed to manage exams of the given course.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -35,6 +36,16 @@ public ExamDateService(ExamRepository examRepository, StudentExamRepository stud
this.studentExamRepository = studentExamRepository;
}

/**
* Returns if the exam is over by checking if the exam end date has passed.
*
* @param exam the exam
* @return true if the exam is over
*/
public boolean isExamOver(Exam exam) {
return exam.getEndDate().isBefore(ZonedDateTime.now());
}

/**
* Returns if the exam is over by checking if the latest individual exam end date plus grace period has passed.
* See {@link ExamDateService#getLatestIndividualExamEndDate}
Expand Down Expand Up @@ -98,8 +109,11 @@ public boolean isIndividualExerciseWorkingPeriodOver(Exam exam, StudentParticipa
if (studentParticipation.isTestRun()) {
return false;
}
// Students can participate in a test exam multiple times, meaning there can be multiple student exams for a single exam.
// For test exams, we aim to find the latest student exam.
// For real exams, we aim to find the only existing student exam.
Optional<StudentExam> optionalStudentExam = studentExamRepository.findFirstByExamIdAndUserIdOrderByIdDesc(exam.getId(), studentParticipation.getParticipant().getId());

var optionalStudentExam = studentExamRepository.findByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId());
if (optionalStudentExam.isPresent()) {
StudentExam studentExam = optionalStudentExam.get();
return Boolean.TRUE.equals(studentExam.isSubmitted()) || studentExam.isEnded();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ public void fetchParticipationsSubmissionsAndResultsForExam(StudentExam studentE
public void filterParticipationForExercise(StudentExam studentExam, Exercise exercise, List<StudentParticipation> participations, boolean isAtLeastInstructor) {
// remove the unnecessary inner course attribute
exercise.setCourse(null);

if (!(exercise instanceof QuizExercise)) {
// Note: quiz exercises are filtered below
exercise.filterSensitiveInformation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,17 @@ public boolean isAllowedToSubmitDuringExam(Exercise exercise, User user, boolean
}

private Optional<StudentExam> findStudentExamForUser(User user, Exam exam) {
// Step 1: Find real exam
Optional<StudentExam> optionalStudentExam = studentExamRepository.findWithExercisesByUserIdAndExamId(user.getId(), exam.getId(), false);
if (optionalStudentExam.isEmpty()) {
// Step 2: Find latest (=the highest id) unsubmitted test exam

Optional<StudentExam> optionalStudentExam;
// Since multiple student exams for a test exam might exist, find the latest (=the highest id) unsubmitted student exam
if (exam.isTestExam()) {
optionalStudentExam = studentExamRepository.findUnsubmittedStudentExamsForTestExamsWithExercisesByExamIdAndUserId(exam.getId(), user.getId()).stream()
.max(Comparator.comparing(StudentExam::getId));
}
else {
// for real exams, there's only one student exam per exam
optionalStudentExam = studentExamRepository.findWithExercisesByUserIdAndExamId(user.getId(), exam.getId(), false);
}
return optionalStudentExam;
}

Expand Down Expand Up @@ -148,8 +152,8 @@ private boolean isExamTestRunSubmission(Exercise exercise, User user, Exam exam)
* @return the submission. If a submission already exists for the exercise we will set the id
*/
public Submission preventMultipleSubmissions(Exercise exercise, Submission submission, User user) {
// Return immediately if it is not an exam submissions or if it is a programming exercise
if (!exercise.isExamExercise() || exercise instanceof ProgrammingExercise) {
// Return immediately if it is not an exam submission or if it is a programming exercise or if it is a test exam exercise
if (!exercise.isExamExercise() || exercise instanceof ProgrammingExercise || exercise.getExam().isTestExam()) {
return submission;
}

Expand Down
Loading
Loading