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 85 commits into
base: develop
Choose a base branch
from

Conversation

coolchock
Copy link
Contributor

@coolchock coolchock commented May 16, 2024

Checklist

General

Server

  • Important: I implemented the changes with a very good performance and prevented too many (unnecessary) database calls.
  • I strictly followed the server coding and design guidelines.
  • I added multiple integration tests (Spring) related to the features (with a high test coverage).
  • I added pre-authorization annotations according to the guidelines and checked the course groups for all new REST Calls (security).
  • I documented the Java code using JavaDoc style.

Client

  • Important: I implemented the changes with a very good performance, prevented too many (unnecessary) REST calls and made sure the UI is responsive, even with large data.
  • I strictly followed the client coding and design guidelines.
  • Following the theming guidelines, I specified colors only in the theming variable files and checked that the changes look consistent in both the light and the dark theme.
  • I added multiple integration tests (Jest) related to the features (with a high test coverage), while following the test guidelines.
  • I added authorities to all new routes and checked the course groups for displaying navigation elements (links, buttons).
  • I documented the TypeScript code using JSDoc style.

Changes affecting Programming Exercises

  • High priority: I tested all changes and their related features with all corresponding user types on a test server configured with the integrated lifecycle setup (LocalVC and LocalCI).
  • I tested all changes and their related features with all corresponding user types on a test server configured with Gitlab and Jenkins.

Motivation and Context

Currently, students are limited to a single attempt per test exam. This pull request introduces a feature that allows students to take the test exam multiple times.

Description

StudentExam now maintains a list of participations for exercises linked to a particular student exam, enabling students to attempt the test exam multiple times. A student can start a new test exam only if the previous one has been submitted or if its working time has expired. The database index in the participation table has been updated to account for the number of attempts. Normal and practice mode exercises have one participation each, while test exam exercises can have up to 256 participations.

Steps for Testing

Prerequisites:

  • 1 Instructor
  • 1 Student
  • 1 Test Exam
  1. Log in to Artemis
  2. Navigate to Course Administration and create a test exam
  3. Go to the course -> exams and try to participate in the test exam multiple times
  4. Verify that each test exam attempt has it's own participations and results.
  5. Important! Please make sure that it's possible to participate in a normal exercise, in a practice mode exercise, in a normal exam and in a test run.

Testserver States

Note

These badges show the state of the test servers.
Green = Currently available, Red = Currently locked






Review Progress

Performance Review

  • I (as a reviewer) confirm that the client changes (in particular related to REST calls and UI responsiveness) are implemented with a very good performance
  • I (as a reviewer) confirm that the server changes (in particular related to database calls) are implemented with a very good performance

Code Review

  • Code Review 1
  • Code Review 2

Manual Tests

  • Test 1
  • Test 2

Exam Mode Test

  • Test 1
  • Test 2

Summary by CodeRabbit

  • New Features

    • Introduced management for test exam attempts, including handling multiple attempts and tracking working time.
  • Enhancements

    • Improved participation creation and initialization processes for test exams.
    • Enhanced methods for finding and retrieving student exams with additional criteria and error handling.
    • Updated exam handling in the sidebar to reflect new properties related to test exams.
  • Bug Fixes

    • Modified route handling and guard for test exam participation to enhance navigation.
  • Tests

    • Updated test cases in ExamParticipationComponent for better handling of test exam scenarios.

@github-actions github-actions bot added tests server Pull requests that update Java code. (Added Automatically!) client Pull requests that update TypeScript code. (Added Automatically!) database Pull requests that update the database. (Added Automatically!). Require a CRITICAL deployment. labels May 16, 2024
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Comment on lines 196 to 210
if (exercise.isTestExamExercise()) {
if (withSubmissions) {
participationOptional = studentParticipationRepository.findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun);
}
else {
participationOptional = studentParticipationRepository.findLatestByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun);
}
}
else {
participationOptional = studentParticipationRepository.findByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun);
if (withSubmissions) {
participationOptional = studentParticipationRepository.findWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun);
}
else {
participationOptional = studentParticipationRepository.findByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun);
}
Copy link

Choose a reason for hiding this comment

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

Ensure consistent error handling across similar methods.

The method findStudentParticipationByExerciseAndStudentLoginAndTestRunOrThrow performs similar operations as findStudentParticipationByExerciseAndStudentLoginOrThrow but does not use the helper method suggested above. This inconsistency can lead to errors if changes are made to one method but not the other. It's crucial to maintain consistency to avoid such issues.

Consider using the suggested refactoring for this method as well to ensure both methods behave similarly and any future changes are centralized.

public ProgrammingExerciseStudentParticipation findStudentParticipationByExerciseAndStudentLoginAndTestRunOrThrow(ProgrammingExercise exercise, String username, boolean isTestRun, boolean withSubmissions) {
    Optional<ProgrammingExerciseStudentParticipation> participationOptional = findParticipationHelper(exercise, username, isTestRun, withSubmissions);
    return participationOptional.orElseThrow(() -> new EntityNotFoundException("Participation could not be found by exerciseId " + exercise.getId() + " and user " + username));
}

Comment on lines 2 to 58
<!-- To clearly indicate a StudentExam within the working time, which can be resumed, the card should be displayed in blue -->
<div
(click)="openStudentExam()"
[ngClass]="{
'row card-body justify-content-center card-general-settings': true,
'bg-primary text-white': withinWorkingTime,
clickable: withinWorkingTime || studentExam.submitted,
}"
>
<div class="row">
<!-- Two variants: Play-Icon, if the studentExam is still within the working time and thus can be resumed.
Magnifying Class for finished StudentExams, to indicate the possibility to review the exam -->
<h4 class="col-sm-auto icon-settings">
@if (withinWorkingTime) {
<fa-icon [icon]="faCirclePlay" size="2x" />
}
@if (!withinWorkingTime && studentExam.submitted) {
<fa-icon [icon]="faMagnifyingGlass" size="2x" />
}
@if (!withinWorkingTime && !studentExam.submitted) {
<fa-icon [icon]="faFileCircleXmark" size="2x" />
}
</h4>
<div class="col-sm">
<div class="row">
<div class="col">
<h5 class="text-start">
{{ 'artemisApp.exam.overview.testExam.' + (withinWorkingTime ? 'resumeAttempt' : 'reviewAttempt') | artemisTranslate: { attempt: index } }}
</h5>
</div>
<div class="col-auto">
@if (withinWorkingTime) {
<div class="text-end">
{{ 'artemisApp.exam.overview.testExam.workingTimeLeft' | artemisTranslate }} {{ workingTimeLeftInSeconds() | artemisDurationFromSeconds: true }}
</div>
}
@if (studentExam.submitted) {
<div>
@if (studentExam.submissionDate) {
<div class="text-end">
{{ 'artemisApp.exam.overview.testExam.submissionDate' | artemisTranslate }} {{ studentExam.submissionDate | artemisDate }}
</div>
}
@if (studentExam.submissionDate && studentExam.startedDate) {
<div class="text-end">
{{ 'artemisApp.exam.overview.testExam.workingTimeCalculated' | artemisTranslate }}
<jhi-testexam-working-time [studentExam]="studentExam" />
</div>
}
</div>
}
<!-- test exams have to be submitted by the students, just as it is the case with real exams -->
@if (!withinWorkingTime && !studentExam.submitted) {
<div>
<div class="text-end">{{ 'artemisApp.exam.overview.testExam.notSubmitted' | artemisTranslate }}</div>
</div>
}
Copy link

Choose a reason for hiding this comment

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

Refactor to simplify conditional class assignment and enhance readability.

The ngClass directive is used extensively to conditionally apply CSS classes based on various conditions. However, the current implementation can be simplified to improve readability and maintainability. Consider using a method in the component's TypeScript file to handle the class logic, which can then be referenced in the template. This approach not only cleans up the template but also centralizes the logic for easier updates in the future.

- [ngClass]="{
-     'row card-body justify-content-center card-general-settings': true,
-     'bg-primary text-white': withinWorkingTime,
-     clickable: withinWorking- Time || studentExam.submitted,
- }"
+ [ngClass]="getClassForStudentExam()"

In your TypeScript file:

getClassForStudentExam() {
  return {
    'row card-body justify-content-center card-general-settings': true,
    'bg-primary text-white': this.withinWorkingTime,
    'clickable': this.withinWorkingTime || this.studentExam.submitted
  };
}

This change encapsulates the logic for class assignment, making the template cleaner and the code easier to manage.

Comment on lines 107 to 117
@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);
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));

Comment on lines +78 to +87
Optional<ProgrammingExerciseStudentParticipation> findFirstByExerciseIdAndStudentLoginOrderByIdDesc(long exerciseId, String username);

default ProgrammingExerciseStudentParticipation findByExerciseIdAndStudentLoginOrThrow(long exerciseId, String username) {
return findByExerciseIdAndStudentLogin(exerciseId, username).orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId));
}

default ProgrammingExerciseStudentParticipation findFirstByExerciseIdAndStudentLoginOrThrow(long exerciseId, String username) {
return findFirstByExerciseIdAndStudentLoginOrderByIdDesc(exerciseId, username)
.orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId));
}
Copy link

Choose a reason for hiding this comment

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

Refactor to use repository methods effectively.

The methods findFirstByExerciseIdAndStudentLoginOrderByIdDesc and findFirstByExerciseIdAndStudentLoginOrThrow are good additions for fetching the latest participation. However, consider using more descriptive method names to reflect that these methods fetch the latest record, which would improve code readability and maintainability.

- findFirstByExerciseIdAndStudentLoginOrderByIdDesc
+ findLatestParticipationByExerciseIdAndStudentLogin

- findFirstByExerciseIdAndStudentLoginOrThrow
+ findLatestParticipationByExerciseIdAndStudentLoginOrThrow
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
Optional<ProgrammingExerciseStudentParticipation> findFirstByExerciseIdAndStudentLoginOrderByIdDesc(long exerciseId, String username);
default ProgrammingExerciseStudentParticipation findByExerciseIdAndStudentLoginOrThrow(long exerciseId, String username) {
return findByExerciseIdAndStudentLogin(exerciseId, username).orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId));
}
default ProgrammingExerciseStudentParticipation findFirstByExerciseIdAndStudentLoginOrThrow(long exerciseId, String username) {
return findFirstByExerciseIdAndStudentLoginOrderByIdDesc(exerciseId, username)
.orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId));
}
Optional<ProgrammingExerciseStudentParticipation> findLatestParticipationByExerciseIdAndStudentLogin(long exerciseId, String username);
default ProgrammingExerciseStudentParticipation findByExerciseIdAndStudentLoginOrThrow(long exerciseId, String username) {
return findByExerciseIdAndStudentLogin(exerciseId, username).orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId));
}
default ProgrammingExerciseStudentParticipation findLatestParticipationByExerciseIdAndStudentLoginOrThrow(long exerciseId, String username) {
return findLatestParticipationByExerciseIdAndStudentLogin(exerciseId, username)
.orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId));
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Comment on lines +145 to +153
// In case of a test exam we don't try to find an existing participation, because students can participate multiple times
// Instead, all previous participations are marked as finished and a new one is created
if (exercise.isTestExamExercise()) {
List<StudentParticipation> participations = studentParticipationRepository.findByExerciseIdAndStudentId(exercise.getId(), participant.getId());
participations.forEach(studentParticipation -> studentParticipation.setInitializationState(InitializationState.FINISHED));
participation = createNewParticipation(exercise, participant);
participation.setNumberOfAttempts(participations.size());
participations.add(participation);
studentParticipationRepository.saveAll(participations);
Copy link

Choose a reason for hiding this comment

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

Refactor suggestion for handling test exam participations

The block of code handling test exam participations is clear in its intent but could be refactored into a separate method to improve readability and maintainability. This would also align with the Single Responsibility Principle by isolating this specific logic.

// This method could be added in the ParticipationService class
private StudentParticipation handleTestExamParticipation(Exercise exercise, Participant participant) {
    List<StudentParticipation> participations = studentParticipationRepository.findByExerciseIdAndStudentId(exercise.getId(), participant.getId());
    participations.forEach(p -> p.setInitializationState(InitializationState.FINISHED));
    StudentParticipation newParticipation = createNewParticipation(exercise, participant);
    newParticipation.setNumberOfAttempts(participations.size());
    participations.add(newParticipation);
    studentParticipationRepository.saveAll(participations);
    return newParticipation;
}

Then replace the code block in startExercise with a call to this new method.

@@ -453,7 +445,7 @@
VersionControlService vcs = versionControlService.orElseThrow();
String templateBranch = vcs.getOrRetrieveBranchOfExercise(programmingExercise);
// the next action includes recovery, which means if the repository has already been copied, we simply retrieve the repository uri and do not copy it again
var newRepoUri = vcs.copyRepository(projectKey, templateRepoName, templateBranch, projectKey, repoName);
var newRepoUri = vcs.copyRepository(projectKey, templateRepoName, templateBranch, projectKey, repoName, participation.getNumberOfAttempts());
Copy link

Choose a reason for hiding this comment

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

Consider handling exceptions for repository operations to enhance robustness

The method copyRepository involves operations that could throw exceptions, such as interactions with external version control services. It's crucial to handle these exceptions to prevent the application from crashing and to provide a better user experience.

try {
    var newRepoUri = vcs.copyRepository(projectKey, templateRepoName, templateBranch, projectKey, repoName, participation.getNumberOfAttempts());
    if (participation.getStudent().isPresent()) {
        newRepoUri = newRepoUri.withUser(participation.getParticipantIdentifier());
    }
    participation.setRepositoryUri(newRepoUri.toString());
    participation.setInitializationState(InitializationState.REPO_COPIED);
} catch (VersionControlException e) {
    log.error("Failed to copy repository: " + e.getMessage());
    // Handle exception appropriately
}

This change ensures that any issues during the repository copying process are logged and handled gracefully.

@@ -179,7 +178,7 @@ public ResponseEntity<StudentExamWithGradeDTO> getStudentExam(@PathVariable Long

examAccessService.checkCourseAndExamAndStudentExamAccessElseThrow(courseId, examId, studentExamId);

StudentExam studentExam = studentExamRepository.findByIdWithExercisesSubmissionPolicyAndSessionsElseThrow(studentExamId);
StudentExam studentExam = studentExamRepository.findByIdWithExercisesAndSessionsElseThrow(studentExamId);
Copy link

Choose a reason for hiding this comment

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

Refactor suggestion for fetching StudentExam with sessions

The method call to findByIdWithExercisesAndSessionsElseThrow seems to be replaced with findByIdWithExercisesAndStudentParticipationsElseThrow based on the AI-generated summary. This change aligns with the PR's goal to handle multiple participations per student exam more effectively.

- StudentExam studentExam = studentExamRepository.findByIdWithExercisesAndSessionsElseThrow(studentExamId);
+ StudentExam studentExam = studentExamRepository.findByIdWithExercisesAndStudentParticipationsElseThrow(studentExamId);

@@ -464,7 +463,7 @@
User user = userRepository.getUserWithGroupsAndAuthorities();
studentExamAccessService.checkCourseAccessForStudentElseThrow(courseId, user);

List<StudentExam> studentExamList = studentExamRepository.findStudentExamForTestExamsByUserIdAndCourseId(user.getId(), courseId);
List<StudentExam> studentExamList = studentExamRepository.findStudentExamsForTestExamsByUserIdAndCourseId(user.getId(), courseId);
Copy link

Choose a reason for hiding this comment

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

Performance optimization for fetching test exams

The method findStudentExamsForTestExamsByUserIdAndCourseId retrieves all student exams for test exams by user ID and course ID. To improve performance and prevent potential memory issues, consider adding pagination or limiting the number of records fetched.

Comment on lines +752 to +753
// Set up new participations for the Exercises
studentExamService.setUpTestExamExerciseParticipationsAndSubmissions(studentExam);
Copy link

Choose a reason for hiding this comment

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

Refactor setup for test exam participations

The logic for setting up new participations for test exams could be encapsulated in a separate method within StudentExamService to enhance modularity and maintainability.

// Encapsulate the setup logic in StudentExamService
studentExamService.setUpTestExamExerciseParticipationsAndSubmissions(studentExam);

Comment on lines +388 to +391
@NotNull
default StudentExam findByIdWithExercisesAndStudentParticipationsElseThrow(Long studentExamId) {
return findWithExercisesAndStudentParticipationsById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId));
}
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));
}

Comment on lines +400 to +401
default StudentExam findByIdWithExercisesAndSessionsElseThrow(Long studentExamId) {
return findWithExercisesAndSessionsById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId));
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));
}

@@ -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.

Comment on lines +764 to +786
@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);
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);

Comment on lines +113 to +123
Optional<StudentExam> optionalStudentExam;
// There are multiple test exams possible for one exam, which implies that there are multiple student exams for one exam.
// For test exams we try to find the latest student exam
// For real exams we try to find the only existing student exam
if (exam.isTestExam()) {
optionalStudentExam = studentExamRepository.findFirstByExamIdAndUserIdOrderByIdDesc(exam.getId(), studentParticipation.getParticipant().getId());
}
else {
optionalStudentExam = studentExamRepository.findByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId());
}

Copy link

Choose a reason for hiding this comment

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

Refactor: Simplify fetching of student exams based on exam type.

The logic to fetch the latest or only student exam based on the exam type is correct but could be simplified. Using a ternary operator might make the code more concise and readable.

-        if (exam.isTestExam()) {
-            optionalStudentExam = studentExamRepository.findFirstByExamIdAndUserIdOrderByIdDesc(exam.getId(), studentParticipation.getParticipant().getId());
-        }
-        else {
-            optionalStudentExam = studentExamRepository.findByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId());
-        }
+        optionalStudentExam = exam.isTestExam()
+            ? studentExamRepository.findFirstByExamIdAndUserIdOrderByIdDesc(exam.getId(), studentParticipation.getParticipant().getId())
+            : studentExamRepository.findByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId());
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
Optional<StudentExam> optionalStudentExam;
// There are multiple test exams possible for one exam, which implies that there are multiple student exams for one exam.
// For test exams we try to find the latest student exam
// For real exams we try to find the only existing student exam
if (exam.isTestExam()) {
optionalStudentExam = studentExamRepository.findFirstByExamIdAndUserIdOrderByIdDesc(exam.getId(), studentParticipation.getParticipant().getId());
}
else {
optionalStudentExam = studentExamRepository.findByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId());
}
Optional<StudentExam> optionalStudentExam;
// There are multiple test exams possible for one exam, which implies that there are multiple student exams for one exam.
// For test exams we try to find the latest student exam
// For real exams we try to find the only existing student exam
optionalStudentExam = exam.isTestExam()
? studentExamRepository.findFirstByExamIdAndUserIdOrderByIdDesc(exam.getId(), studentParticipation.getParticipant().getId())
: studentExamRepository.findByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId());

Copy link
Contributor

@florian-glombik florian-glombik left a comment

Choose a reason for hiding this comment

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

Tested in todays testing session on ts4, for me no issues occurred

The actual UI representation wont work for the new design of exams Exam mode: Redesign the user interface of the exam mode for students #8860 - could you double check if a solution was already implemented in that PR or otherwise make sure to discuss your new solution with @edkaya @rabeatwork ?

The formatting of the attempts could also be more consistent, but as mentioned above this needs to be touched anyways.
image

Copy link
Contributor

@DominikRemo DominikRemo left a comment

Choose a reason for hiding this comment

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

After several attempts, the results for programming exercises become visible. However, after additional attempts, the results disappear for all attempts.
When I participate in a programming exercise and submit a solution during an exam, all previous results for my programming exercises seem to disappear if I hand in the exam before the submission finishes building.

@edkaya
Copy link
Contributor

edkaya commented Jun 27, 2024

Tested in todays testing session on ts4, for me no issues occurred

The actual UI representation wont work for the new design of exams Exam mode: Redesign the user interface of the exam mode for students #8860 - could you double check if a solution was already implemented in that PR or otherwise make sure to discuss your new solution with @edkaya @rabeatwork ?

The formatting of the attempts could also be more consistent, but as mentioned above this needs to be touched anyways. image

No, attempts are not implemented in that PR and we will have a discussion about this after the new exam UI is merged (probably today). I will let you know when we come up with a solution and I can help you integrate your new feature into the new exam UI.

Copy link

github-actions bot commented Jul 6, 2024

There hasn't been any activity on this pull request recently. Therefore, this pull request has been automatically marked as stale and will be closed if no further activity occurs within seven days. Thank you for your contributions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Outside diff range comments (1)
src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts (1)

Line range hint 286-295: Ensure consistency in test descriptions.

The test description "should load existing testExam if studentExam id is start" might be confusing. It should clarify that it verifies the behavior when the studentExam id is set to 'start'.

- it('should load existing testExam if studentExam id is start', () => {
+ it('should load existing testExam if studentExam id is "start"', () => {

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

src/main/webapp/app/types/sidebar.ts Show resolved Hide resolved
src/main/webapp/app/types/sidebar.ts Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
client Pull requests that update TypeScript code. (Added Automatically!) database Pull requests that update the database. (Added Automatically!). Require a CRITICAL deployment. server Pull requests that update Java code. (Added Automatically!) tests
Projects
Status: Ready For Review
Development

Successfully merging this pull request may close these issues.

None yet