Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ public Set<StudentParticipation> findStudentParticipationsInExercise(Set<Student
return Set.of();
}

if (ExerciseType.PROGRAMMING.equals(exercise.getExerciseType())) {
return findStudentParticipationsForProgrammingExercises(participationsInExercise);
if (ExerciseType.PROGRAMMING.equals(exercise.getExerciseType()) || ExerciseType.QUIZ.equals(exercise.getExerciseType())) {
return findStudentParticipationsForMultipleParticipationExercises(participationsInExercise);
}
else {
return Set.of(findStudentParticipationForNonProgrammingExercises(participationsInExercise));
return Set.of(findStudentParticipationForSingleParticipationExercises(participationsInExercise));
}
}

Expand Down Expand Up @@ -96,22 +96,37 @@ public void filterParticipationForCourseDashboard(StudentParticipation participa
participation.setExercise(null);
}

private Set<StudentParticipation> findStudentParticipationsForProgrammingExercises(Set<StudentParticipation> participations) {
/**
* Validates and returns the student participations for exercises that allow multiple participations (programming and quiz),
* which may include at most one graded and one practice participation.
*
* @param participations the set of participations in the exercise to validate
* @return the valid set of participations (empty, only graded, only practice, or both)
* @throws IllegalArgumentException if there are multiple graded or multiple practice participations
*/
private Set<StudentParticipation> findStudentParticipationsForMultipleParticipationExercises(Set<StudentParticipation> participations) {
var gradedParticipations = participations.stream().filter(p -> !p.isPracticeMode()).collect(Collectors.toSet());
if (gradedParticipations.size() > 1) {
throw new IllegalArgumentException("There cannot be more than one graded participation per student for programming exercises");
throw new IllegalArgumentException("There cannot be more than one graded participation per student for programming or quiz exercises");
}
var practiceParticipations = participations.stream().filter(Participation::isPracticeMode).collect(Collectors.toSet());
if (practiceParticipations.size() > 1) {
throw new IllegalArgumentException("There cannot be more than one practice participation per student for programming exercises");
throw new IllegalArgumentException("There cannot be more than one practice participation per student for programming or quiz exercises");
}

return Stream.concat(gradedParticipations.stream(), practiceParticipations.stream()).collect(Collectors.toSet());
}

private StudentParticipation findStudentParticipationForNonProgrammingExercises(Set<StudentParticipation> participations) {
/**
* Validates and returns the student participations for exercises that allow only a single participation (non-programming and non-quiz).
*
* @param participations the set of participations in the exercise to validate
* @return the valid set of participations (empty or singleton set)
* @throws IllegalArgumentException if there are multiple participations
*/
private StudentParticipation findStudentParticipationForSingleParticipationExercises(Set<StudentParticipation> participations) {
if (participations.size() > 1) {
throw new IllegalArgumentException("There cannot be more than one participation per student for non-programming exercises");
throw new IllegalArgumentException("There cannot be more than one participation per student for non-programming or non-quiz exercises");
}
return participations.iterator().next();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ public StudentParticipation startExercise(Exercise exercise, Participant partici

// All other cases, i.e. normal exercises, and regular exam exercises
else {
optionalStudentParticipation = findOneByExerciseAndParticipantAnyState(exercise, participant);
if (exercise instanceof QuizExercise) {
optionalStudentParticipation = findOneByExerciseAndParticipantAnyStateAndTestRun(exercise, participant, false);
}
else {
optionalStudentParticipation = findOneByExerciseAndParticipantAnyState(exercise, participant);
}
if (optionalStudentParticipation.isPresent() && optionalStudentParticipation.get().isPracticeMode() && exercise.isCourseExercise()) {
// In case there is already a practice participation, set it to inactive
optionalStudentParticipation.get().setInitializationState(InitializationState.INACTIVE);
Expand Down Expand Up @@ -272,21 +277,23 @@ private StudentParticipation startProgrammingParticipation(ProgrammingExerciseSt
}

/**
* This method is triggered when a student starts the practice mode of a programming exercise. It creates a Participation which connects the corresponding student and exercise.
* Additionally, it configures repository / build plan related stuff.
* This method is triggered when a student starts the practice mode of an exercise. It creates or reuses a separate StudentParticipation (marked as practiceMode=true)
* connecting the corresponding participant and exercise. For programming exercises, it additionally configures a new repository and build plan (copied from template or
* graded).
* For quiz exercises, it performs simple initialization without VCS or build setup, allowing multiple submissions (no state lock to FINISHED).
* Note: This method finishes any provided graded participation and sets a fixed attempt=1 for practice.
*
* @param exercise the exercise which is started, a programming exercise needs to have the template and solution participation eagerly loaded
* @param participant the user or team who starts the exercise
* @param optionalGradedStudentParticipation the optional graded participation before the due date
* @param useGradedParticipation flag if the graded student participation should be used as baseline for the new repository
* @return the participation connecting the given exercise and user
* @param exercise the exercise to start in practice mode; for programming, template and solution participations should be eagerly loaded
* @param participant the user or team starting practice
* @param optionalGradedStudentParticipation the optional graded (live) participation to finish before starting practice
* @param useGradedParticipation flag if the graded participation's repository should be used as baseline (programming only)
* @return the practice participation connecting the given exercise and participant
*/
public StudentParticipation startPracticeMode(Exercise exercise, Participant participant, Optional<StudentParticipation> optionalGradedStudentParticipation,
boolean useGradedParticipation) {
if (!(exercise instanceof ProgrammingExercise)) {
throw new IllegalStateException("Only programming exercises support the practice mode at the moment");
if (!(exercise instanceof ProgrammingExercise || exercise instanceof QuizExercise)) {
throw new IllegalStateException("Only programming and quiz exercises support the practice mode at the moment");
}

optionalGradedStudentParticipation.ifPresent(participation -> {
participation.setInitializationState(InitializationState.FINISHED);
participationRepository.save(participation);
Expand All @@ -295,13 +302,18 @@ public StudentParticipation startPracticeMode(Exercise exercise, Participant par
StudentParticipation participation;
if (optionalStudentParticipation.isEmpty()) {
// create a new participation only if no participation can be found
participation = new ProgrammingExerciseStudentParticipation(defaultBranch);
if (exercise instanceof ProgrammingExercise) {
participation = new ProgrammingExerciseStudentParticipation(defaultBranch);
}
else {
participation = new StudentParticipation();
}
participation.setInitializationState(InitializationState.UNINITIALIZED);
participation.setExercise(exercise);
participation.setParticipant(participant);
participation.setPracticeMode(true);
participation = studentParticipationRepository.saveAndFlush(participation);
if (participant instanceof User user) {
if (participant instanceof User user && exercise instanceof ProgrammingExercise) {
participationVCSAccessTokenService.createParticipationVCSAccessToken(user, participation);
}
}
Expand All @@ -311,8 +323,20 @@ public StudentParticipation startPracticeMode(Exercise exercise, Participant par
participation.setExercise(exercise);
}

ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exercise.getId());
participation = startPracticeMode(programmingExercise, (ProgrammingExerciseStudentParticipation) participation, optionalGradedStudentParticipation, useGradedParticipation);
if (exercise instanceof ProgrammingExercise) {
ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exercise.getId());
participation = startPracticeMode(programmingExercise, (ProgrammingExerciseStudentParticipation) participation, optionalGradedStudentParticipation,
useGradedParticipation);
}
else {
if (participation.getInitializationState() != InitializationState.INITIALIZED) {
participation.setInitializationState(InitializationState.INITIALIZED);
}
participation.setAttempt(1);
if (participation.getInitializationDate() == null) {
participation.setInitializationDate(ZonedDateTime.now());
}
}

return studentParticipationRepository.saveAndFlush(participation);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import de.tum.cit.aet.artemis.core.util.HeaderUtil;
import de.tum.cit.aet.artemis.exam.api.ExamSubmissionApi;
import de.tum.cit.aet.artemis.exam.config.ExamApiNotPresentException;
import de.tum.cit.aet.artemis.exercise.domain.InitializationState;
import de.tum.cit.aet.artemis.exercise.domain.SubmissionType;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository;
Expand Down Expand Up @@ -157,15 +156,12 @@ public ResponseEntity<Result> submitForPractice(@PathVariable Long exerciseId, @
}

// the following method either reuses an existing participation or creates a new one
StudentParticipation participation = participationService.startExercise(quizExercise, user, false);
StudentParticipation participation = participationService.startPracticeMode(quizExercise, user, Optional.empty(), false);
// we set the exercise again to prevent issues with lazy loaded quiz questions
participation.setExercise(quizExercise);

// update and save submission
Result result = quizSubmissionService.submitForPractice(quizSubmission, quizExercise, participation);
// The quizScheduler is usually responsible for updating the participation to FINISHED in the database. If quizzes where the student did not participate are used for
// practice, the QuizScheduler does not update the participation, that's why we update it manually here
participation.setInitializationState(InitializationState.FINISHED);
studentParticipationRepository.saveAndFlush(participation);

// remove some redundant or unnecessary data that is not needed on client side
Expand Down
Loading
Loading