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

Closed
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
85 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
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
5 changes: 5 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/domain/Exercise.java
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,11 @@ public boolean isExamExercise() {
return this.exerciseGroup != null;
}

@JsonIgnore
public boolean isTestExamExercise() {
return isExamExercise() && this.getExamViaExerciseGroupOrCourseMember().isTestExam();
}

/**
* Utility method to get the course. Get the course over the exerciseGroup, if one was set, otherwise return
* the course class member
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import de.tum.in.www1.artemis.domain.AbstractAuditingEntity;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.domain.quiz.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
coolchock marked this conversation as resolved.
Show resolved Hide resolved
@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<>();
Comment on lines +88 to +92
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

The StudentParticipation class does not contain a corresponding @ManyToMany annotation, which is necessary for a bidirectional relationship. This could lead to issues with the ManyToMany relationship in the StudentExam class.

  • src/main/java/de/tum/in/www1/artemis/domain/participation/StudentParticipation.java
Analysis chain

Ensure that the ManyToMany relationship is correctly configured to handle multiple participations per student exam without redundancy.

Scripts executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Verify the correct configuration of the `ManyToMany` relationship.
# Test: Search for configuration details in the Hibernate or JPA documentation. Expect: Best practices for `ManyToMany` relationships.

cat docs/hibernate_best_practices.md | grep "ManyToMany"

Length of output: 121



Script:

#!/bin/bash
# Search for the `StudentExam` class and its `ManyToMany` annotations.
ast-grep --lang java --pattern 'class StudentExam {
  $$$
  @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<>();
  $$$
}'

Length of output: 619



Script:

#!/bin/bash
# Search for the `StudentExam` class definition in the codebase.
rg 'class StudentExam' --glob '**/*.java'

Length of output: 831



Script:

#!/bin/bash
# Check the `StudentExam` class for `ManyToMany` annotations.
rg '@ManyToMany' src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java

Length of output: 130



Script:

#!/bin/bash
# Search for `StudentParticipation` references in the `StudentExam` class.
rg 'StudentParticipation' src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java

Length of output: 406



Script:

#!/bin/bash
# Search for `@JoinTable`, `@OrderColumn`, and `@Cache` annotations in the `StudentExam` class.
rg '@JoinTable|@OrderColumn|@Cache' src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java

Length of output: 1114



Script:

#!/bin/bash
# Search for the `StudentParticipation` class definition and its annotations.
rg 'class StudentParticipation' --glob '**/*.java'

Length of output: 371



Script:

#!/bin/bash
# Check the `StudentParticipation` class for `ManyToMany` annotations.
rg '@ManyToMany' src/main/java/de/tum/in/www1/artemis/domain/participation/StudentParticipation.java

Length of output: 100


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() {
coolchock marked this conversation as resolved.
Show resolved Hide resolved
return Boolean.TRUE.equals(this.isStarted()) && (Boolean.TRUE.equals(this.isEnded()) || Boolean.TRUE.equals(this.isSubmitted()));
coolchock marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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 @@ -116,6 +116,17 @@ public abstract class Participation extends DomainObject implements Participatio
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
private Set<Submission> submissions = new HashSet<>();

/**
* Graded course exercises, practice mode course exercises and real exam exercises always have only one parcitipation per exercise
* In case of a test exam, there are multiple participations possible for one exercise
* This field is necessary to preserve the constraint of one partipation per exercise, while allowing multiple particpiations per exercise for text exams
coolchock marked this conversation as resolved.
Show resolved Hide resolved
* The value is 0 for graded course exercises and exercises in the real exams
* The value is 1 for practice mode course exercises
* The value is 0-255 for test exam exercises. For each subsequent participation the number is increased by one
*/
@Column(name = "number_of_attempts")
private int numberOfAttempts = 0;
Comment on lines +127 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NumberOfAttempts sounds to me like the maximum number of attempts, but the JavaDoc reads more like this is an indicator on which attempt this is. So just attempt would be a more fitting name imo.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify the comment and adjust the field name for clarity.

The comment and field name numberOfAttempts might be confusing as it suggests the maximum number of attempts, but it actually tracks the current attempt number. Consider renaming the field and updating the comment for clarity.

-    @Column(name = "number_of_attempts")
-    private int numberOfAttempts = 0;
+    @Column(name = "current_attempt")
+    private int currentAttempt = 0;
-     * The value is 0-255 for test exam exercises. For each subsequent participation the number is increased by one
+     * The value ranges from 0-255 for test exam exercises, indicating the current attempt number.


/**
* This property stores the total number of submissions in this participation. Not stored in the database, computed dynamically and used in showing statistics to the user in
* the exercise view.
Expand Down Expand Up @@ -233,6 +244,14 @@ public void setSubmissions(Set<Submission> submissions) {
this.submissions = submissions;
}

public int getNumberOfAttempts() {
return numberOfAttempts;
}

public void setNumberOfAttempts(int index) {
this.numberOfAttempts = index;
}

// jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here, do not remove

/**
Expand Down Expand Up @@ -343,4 +362,5 @@ public String toString() {

@JsonIgnore
public abstract String getType();

}
Original file line number Diff line number Diff line change
Expand Up @@ -508,4 +508,11 @@ 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);

@Query("""
SELECT e.testExam
FROM Exam e
WHERE e.id = :examId
""")
boolean isTestExam(@Param("examId") long examId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ default ProgrammingExerciseStudentParticipation findWithSubmissionsByExerciseIdA

Optional<ProgrammingExerciseStudentParticipation> findByExerciseIdAndStudentLoginAndTestRun(long exerciseId, String username, boolean testRun);

@Query("""
SELECT participation
FROM ProgrammingExerciseStudentParticipation participation
WHERE participation.exercise.id = :exerciseId
AND participation.student.login = :username
AND participation.testRun = :testRun
ORDER BY participation.id DESC
LIMIT 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIMIT is not a generally supported keyword and will break Artemis on Postgres.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You did not answer to this comment yet. We can only merge the PR if the tests are successful with MySQL and Postgres

""")
Optional<ProgrammingExerciseStudentParticipation> findLatestByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username,
@Param("testRun") boolean testRun);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using SQL-specific keywords in JPA queries.

The use of LIMIT in the JPA query might lead to compatibility issues with databases other than MySQL. It's recommended to use Spring Data JPA's Pageable interface to handle limiting results in a more portable way.

- @Query("""
-         SELECT participation
-         FROM ProgrammingExerciseStudentParticipation participation
-         WHERE participation.exercise.id = :exerciseId
-             AND participation.student.login = :username
-             AND participation.testRun = :testRun
-         ORDER BY participation.id DESC
-         LIMIT 1
-         """)
- Optional<ProgrammingExerciseStudentParticipation> findLatestByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username,
-         @Param("testRun") boolean testRun);
+ @Query("""
+         SELECT participation
+         FROM ProgrammingExerciseStudentParticipation participation
+         WHERE participation.exercise.id = :exerciseId
+             AND participation.student.login = :username
+             AND participation.testRun = :testRun
+         ORDER BY participation.id DESC
+         """)
+ Optional<ProgrammingExerciseStudentParticipation> findLatestByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username,
+         @Param("testRun") boolean testRun, Pageable pageable);

And when calling the method:

repository.findLatestByExerciseIdAndStudentLoginAndTestRun(exerciseId, username, testRun, PageRequest.of(0, 1));


@EntityGraph(type = LOAD, attributePaths = { "team.students" })
Optional<ProgrammingExerciseStudentParticipation> findByExerciseIdAndTeamId(long exerciseId, long teamId);

Expand Down Expand Up @@ -154,6 +166,19 @@ List<ProgrammingExerciseStudentParticipation> findWithSubmissionsByExerciseIdAnd
Optional<ProgrammingExerciseStudentParticipation> findWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId,
@Param("username") String username, @Param("testRun") boolean testRun);

@Query("""
SELECT participation
FROM ProgrammingExerciseStudentParticipation participation
LEFT JOIN FETCH participation.submissions
WHERE participation.exercise.id = :exerciseId
AND participation.student.login = :username
AND participation.testRun = :testRun
ORDER BY participation.id DESC
LIMIT 1
""")
Optional<ProgrammingExerciseStudentParticipation> findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId,
@Param("username") String username, @Param("testRun") boolean testRun);

@Query("""
SELECT participation
FROM ProgrammingExerciseStudentParticipation participation
Expand Down
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);
Comment on lines +45 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure proper documentation for the new method findWithExercisesAndStudentParticipationsById.

Adding a Javadoc comment will help other developers understand the purpose of this method quickly, especially since it involves loading multiple entities.


Comment on lines +45 to +47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure proper documentation for the new method findWithExercisesAndStudentParticipationsById.

Adding a Javadoc comment will help other developers understand the purpose of this method quickly, especially since it involves loading multiple entities.

@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> findWithExercisesAndSessionsById(@Param("studentExamId") long studentExamId);
Comment on lines +53 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding Javadoc for findWithExercisesAndSessionsById.

This method efficiently fetches StudentExam along with exercises, sessions, and participations. However, consider adding a brief Javadoc to explain the purpose and usage of this method, especially since it involves multiple joins which could impact performance.


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

@Query("""
SELECT se
FROM StudentExam se
WHERE se.testRun = FALSE
AND se.exam.id = :examId
AND se.user.id = :userId
ORDER BY se.id DESC
LIMIT 1
""")
Optional<StudentExam> findLatestByExamIdAndUserId(@Param("examId") long examId, @Param("userId") 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
* @param testExam boolean indicating if the exam is a test exam
* @return the student exam
* @throws EntityNotFoundException if no student exams could be found
*/
default StudentExam findOneByExamIdAndUserId(long examId, long userId, boolean testExam) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
default StudentExam findOneByExamIdAndUserId(long examId, long userId, boolean testExam) {
default StudentExam findOneByExamIdAndUserIdElseThrow(long examId, long userId, boolean testExam) {

Optional<StudentExam> studentExam;
if (testExam) {
studentExam = this.findLatestByExamIdAndUserId(examId, userId);
}
else {
studentExam = this.findByExamIdAndUserId(examId, userId);
}

return studentExam.orElseThrow(() -> new EntityNotFoundException("StudentExam for exam " + examId + " and user " + userId + " does not exist"));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify documentation for findOneByExamIdAndUserId.

The method documentation is clear about its purpose and the conditions under which it operates, including handling test exams. However, ensure that the documentation explicitly mentions what happens if multiple student exams exist for the same examId and userId combination.


/**
* Checks if any StudentExam exists for the given user (student) id in the given course.
*
Expand Down Expand Up @@ -280,7 +324,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);

@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);
Comment on lines +318 to +328
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure unit tests for new methods handling test exams.

The methods findStudentExamsForTestExamsByUserIdAndCourseId and findStudentExamsForTestExamsByUserIdAndExamId are crucial for the new functionality introduced. It's essential to ensure these methods are covered by unit tests to handle edge cases, especially considering the complex conditions involved.


@Query("""
SELECT DISTINCT se
Expand Down Expand Up @@ -340,15 +394,20 @@ default StudentExam findByIdWithExercisesElseThrow(Long studentExamId) {
return findWithExercisesById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId));
}

@NotNull
default StudentExam findByIdWithExercisesAndStudentParticipationsElseThrow(Long studentExamId) {
return findWithExercisesAndStudentParticipationsById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId));
}
Comment on lines +388 to +391
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add exception details in the Javadoc.

While the method findByIdWithExercisesAndStudentParticipationsElseThrow throws an EntityNotFoundException, the Javadoc does not mention what triggers this exception. Clarifying this in the documentation can help developers understand the conditions under which this exception will be thrown.

/**
 * Get one student exam by id with exercises and student participations.
 * @param studentExamId the id of the student exam
 * @return the student exam with exercises and student participations
 * @throws EntityNotFoundException if the student exam could not be found
 */
@NotNull
default StudentExam findByIdWithExercisesAndStudentParticipationsElseThrow(Long studentExamId) {
    return findWithExercisesAndStudentParticipationsById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId));
}


/**
* Get one student exam by id with exercises, programming exercise submission policy and sessions
* Get one student exam by id with exercises and sessions
*
* @param studentExamId the id of the student exam
* @return the student exam with exercises
*/
@NotNull
default StudentExam findByIdWithExercisesSubmissionPolicyAndSessionsElseThrow(Long studentExamId) {
return findWithExercisesSubmissionPolicyAndSessionsById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId));
default StudentExam findByIdWithExercisesAndSessionsElseThrow(Long studentExamId) {
return findWithExercisesAndSessionsById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId));
Comment on lines +400 to +401
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document exception conditions for findByIdWithExercisesAndSessionsElseThrow.

Similar to the previous method, it's important to document the conditions under which the EntityNotFoundException is thrown for better clarity and maintainability of the code.

/**
 * Get one student exam by id with exercises and sessions.
 * @param studentExamId the id of the student exam
 * @return the student exam with exercises and sessions
 * @throws EntityNotFoundException if no student exams could be found
 */
@NotNull
default StudentExam findByIdWithExercisesAndSessionsElseThrow(Long studentExamId) {
    return findWithExercisesAndSessionsById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId));
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ SELECT COUNT(p.id) > 0
LEFT JOIN p.team.students ts
WHERE p.exercise.course.id = :courseId
AND (p.student.id = :studentId OR ts.id = :studentId)
""")
""")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the JPQL query is complete.

The existsByCourseIdAndStudentId method's JPQL query ends abruptly without a closing parenthesis or a complete WHERE clause. This might lead to a syntax error or unexpected behavior at runtime.

- WHERE p.exercise.course.id = :courseId
+ WHERE p.exercise.course.id = :courseId)

Committable suggestion was skipped due to low confidence.

boolean existsByCourseIdAndStudentId(@Param("courseId") long courseId, @Param("studentId") long studentId);

@Query("""
Expand Down Expand Up @@ -115,6 +115,16 @@ SELECT COUNT(p.id) > 0
""")
Optional<StudentParticipation> findByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username);

@Query("""
SELECT DISTINCT p
FROM StudentParticipation p
WHERE p.exercise.id = :exerciseId
AND p.student.login = :username
ORDER BY p.id DESC
LIMIT 1
""")
Optional<StudentParticipation> findLatestByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace LIMIT clause in JPQL query.

JPQL does not support the LIMIT clause. Use Pageable or set the maximum result size directly in the query method to ensure compatibility and prevent runtime errors.

- LIMIT 1
+ , Pageable pageable

Committable suggestion was skipped due to low confidence.


@Query("""
SELECT DISTINCT p
FROM StudentParticipation p
Expand All @@ -125,6 +135,18 @@ SELECT COUNT(p.id) > 0
""")
Optional<StudentParticipation> findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username);

@Query("""
SELECT DISTINCT p
FROM StudentParticipation p
LEFT JOIN FETCH p.submissions s
WHERE p.exercise.id = :exerciseId
AND p.student.login = :username
AND (s.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR s.type IS NULL)
ORDER BY p.id DESC
LIMIT 1
""")
Optional<StudentParticipation> findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace LIMIT clause in JPQL query.

Similar to the previous comment, the LIMIT clause is not supported in JPQL. Adjusting this to use Pageable or another method to limit results is necessary to avoid syntax errors.

- LIMIT 1
+ , Pageable pageable

Committable suggestion was skipped due to low confidence.


@Query("""
SELECT DISTINCT p
FROM StudentParticipation p
Expand Down Expand Up @@ -425,7 +447,7 @@ default List<StudentParticipation> findByExerciseIdWithManualResultAndFeedbacksA
LEFT JOIN FETCH p.submissions
WHERE p.exercise.id = :exerciseId
AND p.student.id = :studentId
""")
""")
List<StudentParticipation> findByExerciseIdAndStudentIdWithEagerResultsAndSubmissions(@Param("exerciseId") long exerciseId, @Param("studentId") long studentId);

@Query("""
Expand Down Expand Up @@ -744,6 +766,30 @@ List<StudentParticipation> findByStudentIdAndIndividualExercisesWithEagerSubmiss
List<StudentParticipation> findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(@Param("studentId") long studentId,
@Param("exercises") List<Exercise> exercises);

@Query("""
SELECT DISTINCT p
FROM StudentExam se
JOIN se.studentParticipations p
LEFT JOIN FETCH p.submissions s
LEFT JOIN FETCH s.results r
LEFT JOIN FETCH r.assessor
WHERE p.testRun = FALSE
AND se.id IN :studentExamId
""")
List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(
@Param("studentExamId") long studentExamId);

@Query("""
SELECT DISTINCT p
FROM StudentExam se
JOIN se.studentParticipations p
LEFT JOIN FETCH p.submissions s
LEFT JOIN FETCH s.results r
WHERE p.testRun = FALSE
AND se.id IN :studentExamId
""")
List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(@Param("studentExamId") long studentExamId);
coolchock marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +774 to +786
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor to consolidate redundant queries.

The two methods findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns and findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns are very similar and differ only in the fetching of the assessor. Consider refactoring to a single method with a parameter to include or exclude the assessor, reducing redundancy and improving maintainability.

- @Query("""
-         SELECT DISTINCT p
-         FROM StudentExam se
-             JOIN se.studentParticipations p
-             LEFT JOIN FETCH p.submissions s
-             LEFT JOIN FETCH s.results r
-             LEFT JOIN FETCH r.assessor
-         WHERE p.testRun = FALSE
-             AND se.id IN :studentExamId
-         """)
- List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(
-         @Param("studentExamId") long studentExamId);
- 
- @Query("""
-         SELECT DISTINCT p
-             FROM StudentExam se
-                 JOIN se.studentParticipations p
-                 LEFT JOIN FETCH p.submissions s
-                 LEFT JOIN FETCH s.results r
-             WHERE p.testRun = FALSE
-                 AND se.id IN :studentExamId
-         """)
- List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(@Param("studentExamId") long studentExamId);
+ @Query("""
+         SELECT DISTINCT p
+         FROM StudentExam se
+             JOIN se.studentParticipations p
+             LEFT JOIN FETCH p.submissions s
+             LEFT JOIN FETCH s.results r
+             LEFT JOIN FETCH r.assessor
+         WHERE p.testRun = FALSE
+             AND se.id IN :studentExamId
+         """)
+ List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(
+         @Param("studentExamId") long studentExamId, @Param("withAssessor") boolean withAssessor);

Committable suggestion was skipped due to low confidence.

Comment on lines +764 to +786
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor to consolidate redundant queries.

The two methods findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns and findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns are very similar and differ only in the fetching of the assessor. Consider refactoring to a single method with a parameter to include or exclude the assessor, reducing redundancy and improving maintainability.

- @Query("""
-         SELECT DISTINCT p
-         FROM StudentExam se
-             JOIN se.studentParticipations p
-             LEFT JOIN FETCH p.submissions s
-             LEFT JOIN FETCH s.results r
-         WHERE p.testRun = FALSE
-             AND se.id IN :studentExamId
-         """)
- List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(
-         @Param("studentExamId") long studentExamId);
- 
- @Query("""
-         SELECT DISTINCT p
-             FROM StudentExam se
-                 JOIN se.studentParticipations p
-                 LEFT JOIN FETCH p.submissions s
-                 LEFT JOIN FETCH s.results r
-             WHERE p.testRun = FALSE
-                 AND se.id IN :studentExamId
-         """)
- List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(@Param("studentExamId") long studentExamId);
+ @Query("""
+         SELECT DISTINCT p
+         FROM StudentExam se
+             JOIN se.studentParticipations p
+             LEFT JOIN FETCH p.submissions s
+             LEFT JOIN FETCH s.results r
+             LEFT JOIN FETCH r.assessor
+         WHERE p.testRun = FALSE
+             AND se.id IN :studentExamId
+         """)
+ List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(
+         @Param("studentExamId") long studentExamId, @Param("withAssessor") boolean withAssessor);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Query("""
SELECT DISTINCT p
FROM StudentExam se
JOIN se.studentParticipations p
LEFT JOIN FETCH p.submissions s
LEFT JOIN FETCH s.results r
LEFT JOIN FETCH r.assessor
WHERE p.testRun = FALSE
AND se.id IN :studentExamId
""")
List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(
@Param("studentExamId") long studentExamId);
@Query("""
SELECT DISTINCT p
FROM StudentExam se
JOIN se.studentParticipations p
LEFT JOIN FETCH p.submissions s
LEFT JOIN FETCH s.results r
WHERE p.testRun = FALSE
AND se.id IN :studentExamId
""")
List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(@Param("studentExamId") long studentExamId);
@Query("""
SELECT DISTINCT p
FROM StudentExam se
JOIN se.studentParticipations p
LEFT JOIN FETCH p.submissions s
LEFT JOIN FETCH s.results r
LEFT JOIN FETCH r.assessor
WHERE p.testRun = FALSE
AND se.id IN :studentExamId
""")
List<StudentParticipation> findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(
@Param("studentExamId") long studentExamId, @Param("withAssessor") boolean withAssessor);


@Query("""
SELECT DISTINCT p
FROM StudentParticipation p
Expand Down Expand Up @@ -954,7 +1000,7 @@ private List<StudentParticipation> filterParticipationsWithRelevantResults(List<

/**
* Get all participations for the given studentExam and exercises combined with their submissions with a result.
* Distinguishes between student exams and test runs and only loads the respective participations
* Distinguishes between real exams, test exams and test runs and only loads the respective participations
*
* @param studentExam studentExam with exercises loaded
* @param withAssessor (only for non-test runs) if assessor should be loaded with the result
Expand All @@ -964,6 +1010,16 @@ default List<StudentParticipation> findByStudentExamWithEagerSubmissionsResult(S
if (studentExam.isTestRun()) {
return findTestRunParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResult(studentExam.getUser().getId(), studentExam.getExercises());
}

if (studentExam.isTestExam()) {
if (withAssessor) {
return findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(studentExam.getId());
}
else {
return findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(studentExam.getId());
}
}

coolchock marked this conversation as resolved.
Show resolved Hide resolved
else {
if (withAssessor) {
return findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(studentExam.getUser().getId(), studentExam.getExercises());
Expand Down
Loading
Loading