-
Notifications
You must be signed in to change notification settings - Fork 275
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
base: develop
Are you sure you want to change the base?
Exam mode
: Enable students to participate in the test exam multiple times
#8609
Conversation
…d and exercise_id
There was a problem hiding this 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
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); | ||
} |
There was a problem hiding this comment.
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));
}
<!-- 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> | ||
} |
There was a problem hiding this comment.
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.
@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); |
There was a problem hiding this comment.
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));
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)); | ||
} |
There was a problem hiding this comment.
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.
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)); | |
} |
There was a problem hiding this 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
// 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); |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
// Set up new participations for the Exercises | ||
studentExamService.setUpTestExamExerciseParticipationsAndSubmissions(studentExam); |
There was a problem hiding this comment.
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);
@NotNull | ||
default StudentExam findByIdWithExercisesAndStudentParticipationsElseThrow(Long studentExamId) { | ||
return findWithExercisesAndStudentParticipationsById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId)); | ||
} |
There was a problem hiding this comment.
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));
}
default StudentExam findByIdWithExercisesAndSessionsElseThrow(Long studentExamId) { | ||
return findWithExercisesAndSessionsById(studentExamId).orElseThrow(() -> new EntityNotFoundException("Student exam", studentExamId)); |
There was a problem hiding this comment.
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) | |||
""") | |||
""") |
There was a problem hiding this comment.
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.
@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); |
There was a problem hiding this comment.
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.
@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); |
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()); | ||
} | ||
|
There was a problem hiding this comment.
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.
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()); |
There was a problem hiding this 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.
There was a problem hiding this 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.
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. |
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. |
There was a problem hiding this 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
src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.html
Show resolved
Hide resolved
There was a problem hiding this 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"', () => {
src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts
Show resolved
Hide resolved
src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts
Show resolved
Hide resolved
src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts
Show resolved
Hide resolved
src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts
Show resolved
Hide resolved
src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts
Show resolved
Hide resolved
There was a problem hiding this 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
Checklist
General
Server
Client
authorities
to all new routes and checked the course groups for displaying navigation elements (links, buttons).Changes affecting Programming Exercises
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:
Testserver States
Note
These badges show the state of the test servers.
Green = Currently available, Red = Currently locked
Review Progress
Performance Review
Code Review
Manual Tests
Exam Mode Test
Summary by CodeRabbit
New Features
Enhancements
Bug Fixes
Tests
ExamParticipationComponent
for better handling of test exam scenarios.