From 7392a092d776de174595eb1d94241b39ac075410 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 13 May 2024 19:57:27 +0200 Subject: [PATCH 01/73] refactor StudentExamService --- .../service/exam/StudentExamService.java | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java index 38fdf510749d..0ca943aca7e8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java @@ -635,25 +635,21 @@ private void setUpTestRunExerciseParticipationsAndSubmissions(Long testRunId) { * Method to set up new participations for a StudentExam of a test exam. * * @param studentExam the studentExam for which the new participations should be set up - * @param startedDate the Date to which the InitializationDate should be set, in order to link StudentExam <-> participation */ - public void setUpTestExamExerciseParticipationsAndSubmissions(StudentExam studentExam, ZonedDateTime startedDate) { + public void setUpTestExamExerciseParticipationsAndSubmissions(StudentExam studentExam) { List generatedParticipations = Collections.synchronizedList(new ArrayList<>()); - setUpExerciseParticipationsAndSubmissionsWithInitializationDate(studentExam, generatedParticipations, startedDate); + setUpExerciseParticipationsAndSubmissions(studentExam, generatedParticipations); // TODO: Michael Allgaier: schedule a lock operation for all involved student repositories of this student exam (test exam) at the end of the individual working time studentParticipationRepository.saveAll(generatedParticipations); } /** - * Helper-Method for the Set up process of an StudentExam with a given startedDate. The method forces a new participation for every exercise, - * unlocks the Repository in case the StudentExam starts in less than 5mins and returns the generated participations + * Sets up the participations and submissions for all the exercises of the student exam. * - * @param studentExam the studentExam for which the new participations should be set up - * @param generatedParticipations the list where the newly generated participations should be added - * @param startedDate the Date to which the InitializationDate should be set, in order to link StudentExam <-> participation + * @param studentExam The studentExam for which the participations and submissions should be created + * @param generatedParticipations List of generated participations to track how many participations have been generated */ - private void setUpExerciseParticipationsAndSubmissionsWithInitializationDate(StudentExam studentExam, List generatedParticipations, - ZonedDateTime startedDate) { + public void setUpExerciseParticipationsAndSubmissions(StudentExam studentExam, List generatedParticipations) { User student = studentExam.getUser(); for (Exercise exercise : studentExam.getExercises()) { @@ -674,14 +670,8 @@ private void setUpExerciseParticipationsAndSubmissionsWithInitializationDate(Stu programmingExercise.setTemplateParticipation(programmingExerciseReloaded.getTemplateParticipation()); } // this will also create initial (empty) submissions for quiz, text, modeling and file upload - // If the startedDate is provided, the InitializationDate is set to the startedDate - StudentParticipation participation; - if (startedDate != null) { - participation = participationService.startExerciseWithInitializationDate(exercise, student, true, startedDate); - } - else { - participation = participationService.startExercise(exercise, student, true); - } + StudentParticipation participation = participationService.startExercise(exercise, student, true); + generatedParticipations.add(participation); // Unlock repository and participation only if the real exam starts within 5 minutes or if we have a test exam or test run if (participation instanceof ProgrammingExerciseStudentParticipation programmingParticipation && exercise instanceof ProgrammingExercise programmingExercise) { @@ -786,16 +776,6 @@ public Optional getExerciseStartStatusOfExam .map(wrapper -> (ExamExerciseStartPreparationStatus) wrapper.get()); } - /** - * Sets up the participations and submissions for all the exercises of the student exam. - * - * @param studentExam The studentExam for which the participations and submissions should be created - * @param generatedParticipations List of generated participations to track how many participations have been generated - */ - public void setUpExerciseParticipationsAndSubmissions(StudentExam studentExam, List generatedParticipations) { - setUpExerciseParticipationsAndSubmissionsWithInitializationDate(studentExam, generatedParticipations, null); - } - /** * Generates a new test exam for the student and stores it in the database * From 89ef7fbf8c1c738ec9d710c9419408837a76e914 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 13 May 2024 19:58:04 +0200 Subject: [PATCH 02/73] refactor ParticipationService^ --- .../artemis/service/ParticipationService.java | 34 ++++--------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index a6e444540ef6..9075fbf5c225 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -138,24 +138,6 @@ public ParticipationService(GitService gitService, Optional participation - * @return a new participation for the given exercise and user - */ - // TODO: Stephan Krusche: offer this method again like above "startExercise" without initializationDate which is not really necessary at the moment, because we only support on - // test exam per exam/student - public StudentParticipation startExerciseWithInitializationDate(Exercise exercise, Participant participant, boolean createInitialSubmission, ZonedDateTime initializationDate) { // common for all exercises Optional optionalStudentParticipation = findOneByExerciseAndParticipantAnyState(exercise, participant); if (optionalStudentParticipation.isPresent() && optionalStudentParticipation.get().isPracticeMode() && exercise.isCourseExercise()) { @@ -169,7 +151,7 @@ public StudentParticipation startExerciseWithInitializationDate(Exercise exercis // Check if participation already exists StudentParticipation participation; if (optionalStudentParticipation.isEmpty()) { - participation = createNewParticipationWithInitializationDate(exercise, participant, initializationDate); + participation = createNewParticipation(exercise, participant); } else { // make sure participation and exercise are connected @@ -179,7 +161,7 @@ public StudentParticipation startExerciseWithInitializationDate(Exercise exercis if (exercise instanceof ProgrammingExercise programmingExercise) { // fetch again to get additional objects - participation = startProgrammingExercise(programmingExercise, (ProgrammingExerciseStudentParticipation) participation, initializationDate == null); + participation = startProgrammingExercise(programmingExercise, (ProgrammingExerciseStudentParticipation) participation, false); } else {// for all other exercises: QuizExercise, ModelingExercise, TextExercise, FileUploadExercise if (participation.getInitializationState() == null || participation.getInitializationState() == InitializationState.UNINITIALIZED @@ -208,12 +190,11 @@ public StudentParticipation startExerciseWithInitializationDate(Exercise exercis /** * Helper Method to create a new Participation for the * - * @param exercise the exercise for which a participation should be created - * @param participant the participant for the participation - * @param initializationDate (optional) Value for the initializationDate of the Participation + * @param exercise the exercise for which a participation should be created + * @param participant the participant for the participation * @return a StudentParticipation for the exercise and participant with an optional specified initializationDate */ - private StudentParticipation createNewParticipationWithInitializationDate(Exercise exercise, Participant participant, ZonedDateTime initializationDate) { + private StudentParticipation createNewParticipation(Exercise exercise, Participant participant) { StudentParticipation participation; // create a new participation only if no participation can be found if (exercise instanceof ProgrammingExercise) { @@ -225,10 +206,7 @@ private StudentParticipation createNewParticipationWithInitializationDate(Exerci participation.setInitializationState(InitializationState.UNINITIALIZED); participation.setExercise(exercise); participation.setParticipant(participant); - // StartedDate is used to link a Participation to a test exam exercise - if (initializationDate != null) { - participation.setInitializationDate(initializationDate); - } + return studentParticipationRepository.saveAndFlush(participation); } From 61cba390452cfc719fc52bae4bdf93ec1d2c3156 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 13 May 2024 23:30:20 +0200 Subject: [PATCH 03/73] add student_exam_participation table --- .../www1/artemis/domain/exam/StudentExam.java | 15 +++++++++++++++ .../changelog/20240513101552_changelog.xml | 18 ++++++++++++++++++ src/main/resources/config/liquibase/master.xml | 1 + 3 files changed, 34 insertions(+) create mode 100644 src/main/resources/config/liquibase/changelog/20240513101552_changelog.xml diff --git a/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java b/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java index 537a37dafa17..a7984741d1ae 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java @@ -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 @@ -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 quizQuestions = new ArrayList<>(); + @ManyToMany + @JoinTable(name = "student_exam_participation", joinColumns = @JoinColumn(name = "student_exam_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "participation_id", referencedColumnName = "id")) + @OrderColumn(name = "participation_order") + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + private List studentParticipations = new ArrayList<>(); + public Boolean isSubmitted() { return submitted; } @@ -199,6 +206,14 @@ public void setQuizQuestions(List quizQuestions) { this.quizQuestions = quizQuestions; } + public List getStudentParticipations() { + return studentParticipations; + } + + public void setStudentParticipations(List studentParticipations) { + this.studentParticipations = studentParticipations; + } + /** * Adds the given exam session to the student exam * diff --git a/src/main/resources/config/liquibase/changelog/20240513101552_changelog.xml b/src/main/resources/config/liquibase/changelog/20240513101552_changelog.xml new file mode 100644 index 000000000000..25159c5c8242 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240513101552_changelog.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 4711d210aa25..e534ab0e260a 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -8,6 +8,7 @@ + From 8da735cf7a2bdddb21fa8f847fe05c4ff6852518 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 14 May 2024 23:33:21 +0200 Subject: [PATCH 04/73] assign generated participations to the student exam --- .../tum/in/www1/artemis/service/exam/StudentExamService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java index 0ca943aca7e8..bac8bfe6f539 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java @@ -640,6 +640,8 @@ public void setUpTestExamExerciseParticipationsAndSubmissions(StudentExam studen List generatedParticipations = Collections.synchronizedList(new ArrayList<>()); setUpExerciseParticipationsAndSubmissions(studentExam, generatedParticipations); // TODO: Michael Allgaier: schedule a lock operation for all involved student repositories of this student exam (test exam) at the end of the individual working time + studentExam.setStudentParticipations(generatedParticipations); + studentExamRepository.save(studentExam); studentParticipationRepository.saveAll(generatedParticipations); } @@ -661,7 +663,7 @@ public void setUpExerciseParticipationsAndSubmissions(StudentExam studentExam, L // TODO: directly check in the database if the entry exists for the student, exercise and InitializationState.INITIALIZED var studentParticipations = participationService.findByExerciseAndStudentId(exercise, student.getId()); // we start the exercise if no participation was found that was already fully initialized - if (studentParticipations.stream().noneMatch(studentParticipation -> studentParticipation.getParticipant().equals(student) + if (studentExam.isTestExam() || studentParticipations.stream().noneMatch(studentParticipation -> studentParticipation.getParticipant().equals(student) && studentParticipation.getInitializationState() != null && studentParticipation.getInitializationState().hasCompletedState(InitializationState.INITIALIZED))) { try { // Load lazy property From 07c541e7fa243a829317a580f56cb4b99f75fe39 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 14 May 2024 23:35:16 +0200 Subject: [PATCH 05/73] refactor start exercise method --- .../artemis/service/ParticipationService.java | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index 9075fbf5c225..1d4512a8decf 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -138,32 +138,47 @@ public ParticipationService(GitService gitService, Optional 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); - studentParticipationRepository.saveAndFlush(optionalStudentParticipation.get()); - - optionalStudentParticipation = findOneByExerciseAndParticipantAnyStateAndTestRun(exercise, participant, false); - } - // Check if participation already exists StudentParticipation participation; - if (optionalStudentParticipation.isEmpty()) { + Optional optionalStudentParticipation = Optional.empty(); + + // In case of a test exam we don't try 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.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { + List participations = studentParticipationRepository.findByExerciseIdAndStudentId(exercise.getId(), participant.getId()); + participations.forEach(studentParticipation -> studentParticipation.setInitializationState(InitializationState.FINISHED)); participation = createNewParticipation(exercise, participant); + studentParticipationRepository.saveAll(participations); } + + // All other cases, i.e. normal exercises, and regular exam exercises else { - // make sure participation and exercise are connected - participation = optionalStudentParticipation.get(); - participation.setExercise(exercise); + // common for all exercises + 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); + studentParticipationRepository.saveAndFlush(optionalStudentParticipation.get()); + + optionalStudentParticipation = findOneByExerciseAndParticipantAnyStateAndTestRun(exercise, participant, false); + } + // Check if participation already exists + if (optionalStudentParticipation.isEmpty()) { + participation = createNewParticipation(exercise, participant); + } + else { + // make sure participation and exercise are connected + participation = optionalStudentParticipation.get(); + participation.setExercise(exercise); + } } if (exercise instanceof ProgrammingExercise programmingExercise) { // fetch again to get additional objects participation = startProgrammingExercise(programmingExercise, (ProgrammingExerciseStudentParticipation) participation, false); } - else {// for all other exercises: QuizExercise, ModelingExercise, TextExercise, FileUploadExercise + // for all other exercises: QuizExercise, ModelingExercise, TextExercise, FileUploadExercise + else { if (participation.getInitializationState() == null || participation.getInitializationState() == InitializationState.UNINITIALIZED || participation.getInitializationState() == InitializationState.FINISHED && !(exercise instanceof QuizExercise)) { // in case the participation was finished before, we set it to initialized again so that the user sees the correct button "Open modeling editor" on the client side. @@ -626,6 +641,10 @@ public Optional findOneByExerciseAndStudentLoginWithEagerS return studentParticipationRepository.findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); } + public Optional findLatestByExerciseAndStudentLoginWithEagerSubmissionsAnyState(Exercise exercise, String username) { + return studentParticipationRepository.findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); + } + /** * Get all exercise participations belonging to exercise and student. * From c6ced67480ecd9bc0c674c8d6d8a72c413ceeac4 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 14 May 2024 23:36:22 +0200 Subject: [PATCH 06/73] remove index to allow multiple participations with the same student_id and exercise_id --- .../liquibase/changelog/20240513101555_changelog.xml | 8 ++++++++ src/main/resources/config/liquibase/master.xml | 1 + 2 files changed, 9 insertions(+) create mode 100644 src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml diff --git a/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml b/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml new file mode 100644 index 000000000000..98e9fd477aa4 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index e534ab0e260a..484942d9e00c 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -9,6 +9,7 @@ + From fff4bfa4da3d7830d7484b81ad83c6fe36060ed2 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 14 May 2024 23:37:10 +0200 Subject: [PATCH 07/73] refactor exam access service --- .../service/exam/ExamAccessService.java | 100 ++++++++++++++---- 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java index f5d3d803d6ed..77db0a2f8114 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java @@ -3,6 +3,7 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; import java.time.ZonedDateTime; +import java.util.List; import java.util.Optional; import org.springframework.context.annotation.Profile; @@ -67,38 +68,46 @@ public ExamAccessService(ExamRepository examRepository, StudentExamRepository st * @param examId The id of the exam * @return a ResponseEntity with the exam */ + public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { User currentUser = userRepository.getUserWithGroupsAndAuthorities(); - - // TODO: we should distinguish the whole method between test exam and real exam to improve the readability of the code // Check that the current user is at least student in the course. Course course = courseRepository.findByIdElseThrow(courseId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, currentUser); - // Check that the student exam exists - Optional optionalStudentExam = studentExamRepository.findByExamIdAndUserId(examId, currentUser.getId()); - + Exam exam = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examId); StudentExam studentExam; - // If an studentExam can be fund, we can proceed - if (optionalStudentExam.isPresent()) { - studentExam = optionalStudentExam.get(); + + if (exam.isTestExam()) { + List unfinishedStudentExams = studentExamRepository.findStudentExamForTestExamsByUserIdAndCourseId(currentUser.getId(), courseId).stream() + .filter(attempt -> !attempt.isFinished()).toList(); + if (unfinishedStudentExams.isEmpty()) { + studentExam = studentExamService.generateTestExam(exam, currentUser); + // For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource + studentExam.setExercises(null); + } + // TODO Michal Kawka is it possible, that the list has more elements. If yes, throw an error? + else { + studentExam = unfinishedStudentExams.getFirst(); + } + // Check that the current user is registered for the test exam. Otherwise, the student can self-register + examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course, exam.getId(), currentUser); + } else { - // Only Test Exams can be self-created by the user. - Exam examWithExerciseGroupsAndExercises = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examId); - - if (!examWithExerciseGroupsAndExercises.isTestExam()) { + // Check that the student exam exists + Optional optionalStudentExam = studentExamRepository.findByExamIdAndUserId(examId, currentUser.getId()); + // If an studentExam can be found, we can proceed + if (optionalStudentExam.isPresent()) { + studentExam = optionalStudentExam.get(); + } + else { // We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME, "StudentExamGenerationOnlyForTestExams", true); } - studentExam = studentExamService.generateTestExam(examWithExerciseGroupsAndExercises, currentUser); - // For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource - studentExam.setExercises(null); } - Exam exam = studentExam.getExam(); - checkExamBelongsToCourseElseThrow(courseId, exam); if (!examId.equals(exam.getId())) { @@ -110,15 +119,64 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { throw new AccessForbiddenException(ENTITY_NAME, examId); } - if (exam.isTestExam()) { - // Check that the current user is registered for the test exam. Otherwise, the student can self-register - examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course, exam.getId(), currentUser); - } // NOTE: the check examRepository.isUserRegisteredForExam is not necessary because we already checked before that there is a student exam in this case for the current user return studentExam; } + // public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { + // User currentUser = userRepository.getUserWithGroupsAndAuthorities(); + // + // // TODO: we should distinguish the whole method between test exam and real exam to improve the readability of the code + // // Check that the current user is at least student in the course. + // Course course = courseRepository.findByIdElseThrow(courseId); + // authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, currentUser); + // Exam exam = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examId); + // StudentExam studentExam; + // + // if (exam.isTestExam()) { + // List unsubmittedStudentExams = studentExamRepository.findUnsubmittedStudentExamsForTestExamsByExamIdAndUserId(examId, currentUser.getId()); + // if (unsubmittedStudentExams.isEmpty()) { + // studentExam = studentExamService.generateTestExam(exam, currentUser); + // // For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource + // studentExam.setExercises(null); + // } + // else { + // studentExam = unsubmittedStudentExams.getFirst(); + // } + // // Check that the current user is registered for the test exam. Otherwise, the student can self-register + // examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course, exam.getId(), currentUser); + // + // } + // else { + // // Check that the student exam exists + // Optional optionalStudentExam = studentExamRepository.findByExamIdAndUserId(examId, currentUser.getId()); + // if (optionalStudentExam.isPresent()) { + // studentExam = optionalStudentExam.get(); + // } + // else { + // // We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam + // throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME, + // "StudentExamGenerationOnlyForTestExams", true); + // } + // } + // + // checkExamBelongsToCourseElseThrow(courseId, exam); + // + // if (!examId.equals(exam.getId())) { + // throw new BadRequestAlertException("The provided examId does not match with the examId of the studentExam", ENTITY_NAME, "examIdMismatch"); + // } + // + // // Check that the exam is visible + // if (exam.getVisibleDate() != null && exam.getVisibleDate().isAfter(ZonedDateTime.now())) { + // throw new AccessForbiddenException(ENTITY_NAME, examId); + // } + // + // // NOTE: the check examRepository.isUserRegisteredForExam is not necessary because we already checked before that there is a student exam in this case for the current user + // + // return studentExam; + // } + /** * Checks if the current user is allowed to manage exams of the given course. * From 6dc3c42e9f08c3deb26e95855e30b35813bc5363 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 08:03:03 +0200 Subject: [PATCH 08/73] add participation_id column to participation table --- .../liquibase/changelog/20240513101555_changelog.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml b/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml index 98e9fd477aa4..afeb8b7f63f6 100644 --- a/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml @@ -3,6 +3,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> - + + + + + \ No newline at end of file From 4b905468eba072e89d9704254f7c9fb1787395e7 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 08:03:24 +0200 Subject: [PATCH 09/73] add participation_id column to the index --- .../changelog/20240513101557_changelog.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/resources/config/liquibase/changelog/20240513101557_changelog.xml diff --git a/src/main/resources/config/liquibase/changelog/20240513101557_changelog.xml b/src/main/resources/config/liquibase/changelog/20240513101557_changelog.xml new file mode 100644 index 000000000000..9219ae5e7e85 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240513101557_changelog.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file From b882350baf5e3b49169c804e1bb2e1601c349797 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 12:11:22 +0200 Subject: [PATCH 10/73] add a query to fetch last text exam participation --- .../repository/StudentParticipationRepository.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 3bc218392267..9f960a9d636f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -125,6 +125,17 @@ SELECT COUNT(p.id) > 0 """) Optional 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 p.id = (SELECT MAX(p2.id) FROM StudentParticipation p2 WHERE p2.exercise.id = :exerciseId AND p2.student.login = :username) + AND (s.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR s.type IS NULL) + """) + Optional findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username); + @Query(""" SELECT DISTINCT p FROM StudentParticipation p From ffb9eabe01dc93aaea460ce9ba476ffa96eb8f0e Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 12:12:44 +0200 Subject: [PATCH 11/73] add changelogs to master.xml --- src/main/resources/config/liquibase/master.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 484942d9e00c..c0cd27bce51e 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -10,6 +10,7 @@ + From 691265946d52bdf446edf53b1549244311f94947 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 12:16:22 +0200 Subject: [PATCH 12/73] change navigation in the attempt-review-component --- .../course-exam-attempt-review-detail.component.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts index b6e72077f58c..80df750bebe6 100644 --- a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts +++ b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts @@ -85,12 +85,17 @@ export class CourseExamAttemptReviewDetailComponent implements OnInit, OnDestroy } /** - * navigate to /courses/:courseId/exams/:examId/test-exam/:studentExamId + * navigate to /courses/:courseId/exams/:examId/test-exam/:studentExamId if the attempt is either submitted or the time is up + * navigate to /courses/:courseId/exams/:examId/test-exam/start if the attempt can be continued * Used to open the corresponding studentExam */ openStudentExam(): void { - if (this.studentExam.submitted || this.withinWorkingTime) { - this.router.navigate(['courses', this.courseId, 'exams', this.exam.id, 'test-exam', this.studentExam.id]); + // If exam is submitted or the time us up, navigate to the exam overview + // else navigate to the exam start page to continue the attempt + if (this.studentExam.submitted || !this.withinWorkingTime) { + this.router.navigate(['courses', this.courseId, 'exams', this.exam.id, 'test-exam', this.studentExam.id]); /// + } else { + this.router.navigate(['courses', this.courseId, 'exams', this.exam.id, 'test-exam', 'start']); } } } From 261c3be25a42d5b4dba8c027fc6a6205ff108393 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 12:35:40 +0200 Subject: [PATCH 13/73] refactor ExamSubmissionService --- .../artemis/service/exam/ExamSubmissionService.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java index 443e2d35e233..34ef5f84263d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java @@ -102,13 +102,17 @@ public boolean isAllowedToSubmitDuringExam(Exercise exercise, User user, boolean } private Optional findStudentExamForUser(User user, Exam exam) { - // Step 1: Find real exam - Optional optionalStudentExam = studentExamRepository.findWithExercisesByUserIdAndExamId(user.getId(), exam.getId(), false); - if (optionalStudentExam.isEmpty()) { - // Step 2: Find latest (=the highest id) unsubmitted test exam + + Optional optionalStudentExam; + // Since multiple student exams for a test exam might exist, find the latest (=the highest id) unsubmitted student exam + if (exam.isTestExam()) { optionalStudentExam = studentExamRepository.findUnsubmittedStudentExamsForTestExamsWithExercisesByExamIdAndUserId(exam.getId(), user.getId()).stream() .max(Comparator.comparing(StudentExam::getId)); } + else { + // for real exams, there's only one student exam per exam + optionalStudentExam = studentExamRepository.findWithExercisesByUserIdAndExamId(user.getId(), exam.getId(), false); + } return optionalStudentExam; } From 43b0ef163200abe2b302ff343e1ab97d455baab3 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 12:49:25 +0200 Subject: [PATCH 14/73] refactor ExamDateService --- .../www1/artemis/service/exam/ExamDateService.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java index 76261d79d2df..dd0faafbc763 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java @@ -5,6 +5,7 @@ import java.time.ZonedDateTime; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -99,7 +100,17 @@ public boolean isIndividualExerciseWorkingPeriodOver(Exam exam, StudentParticipa return false; } - var optionalStudentExam = studentExamRepository.findByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId()); + Optional 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.findLatestByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId()); + } + else { + optionalStudentExam = studentExamRepository.findByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId()); + } + if (optionalStudentExam.isPresent()) { StudentExam studentExam = optionalStudentExam.get(); return Boolean.TRUE.equals(studentExam.isSubmitted()) || studentExam.isEnded(); From c055ef14bbcec12d76f982b127f31b6d246332f9 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 13:06:20 +0200 Subject: [PATCH 15/73] add isFinished method in the StudentExam class --- .../java/de/tum/in/www1/artemis/domain/exam/StudentExam.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java b/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java index a7984741d1ae..7a1c319e8fcf 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java @@ -246,6 +246,10 @@ public Boolean isEnded() { return ZonedDateTime.now().isAfter(getIndividualEndDate()); } + public boolean isFinished() { + return Boolean.TRUE.equals(this.isStarted()) && (Boolean.TRUE.equals(this.isEnded()) || Boolean.TRUE.equals(this.isSubmitted())); + } + /** * Returns the individual exam end date taking the working time of this student exam into account. * For test exams, the startedDate needs to be defined as this is not equal to exam.startDate From 9e2e08cb8f2a5baeea649062bae434021384a469 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 13:43:25 +0200 Subject: [PATCH 16/73] change column name from participation_id to number_of_attempts --- .../config/liquibase/changelog/20240513101555_changelog.xml | 2 +- .../config/liquibase/changelog/20240513101557_changelog.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml b/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml index afeb8b7f63f6..c5052078b551 100644 --- a/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240513101555_changelog.xml @@ -5,7 +5,7 @@ diff --git a/src/main/resources/config/liquibase/changelog/20240513101557_changelog.xml b/src/main/resources/config/liquibase/changelog/20240513101557_changelog.xml index 9219ae5e7e85..7bd2e6bd6d71 100644 --- a/src/main/resources/config/liquibase/changelog/20240513101557_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20240513101557_changelog.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file From dd4e8a71b62717c5dcc5ae1e8812fca2e972c96c Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 13:44:07 +0200 Subject: [PATCH 17/73] add query to check if an exam is a test exam --- .../de/tum/in/www1/artemis/repository/ExamRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java index 7c05bc7c8f4e..b6ff3ccaa2c3 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java @@ -513,4 +513,12 @@ private static Map convertListOfCountsIntoMap(List examId AND registeredUsers.user.id = :userId """) Set findActiveExams(@Param("courseIds") Set courseIds, @Param("userId") long userId, @Param("visible") ZonedDateTime visible, @Param("end") ZonedDateTime end); + + @Query(""" + SELECT COUNT(e) > 0 + FROM Exam e + WHERE e.id = :examId + AND e.testExam + """) + boolean isTestExam(@Param("examId") long examId); } From e5525780d00efb919e9e0e58e5aed034974ece2a Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 13:49:55 +0200 Subject: [PATCH 18/73] add column to Participation entity --- .../domain/participation/Participation.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java index ada17c5371e5..f81e0953d70c 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java @@ -116,6 +116,17 @@ public abstract class Participation extends DomainObject implements Participatio @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) private Set 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 + * 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; + /** * 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. @@ -233,6 +244,14 @@ public void setSubmissions(Set 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 /** @@ -343,4 +362,5 @@ public String toString() { @JsonIgnore public abstract String getType(); + } From 1ab81f1e93d0fceed81a5d8a48408608110277af Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 13:51:11 +0200 Subject: [PATCH 19/73] set number of attempts --- .../www1/artemis/service/ParticipationService.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index 1d4512a8decf..c97bdcc46103 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -142,12 +142,14 @@ public StudentParticipation startExercise(Exercise exercise, Participant partici StudentParticipation participation; Optional optionalStudentParticipation = Optional.empty(); - // In case of a test exam we don't try find an existing participation, because students can participate multiple times + // 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.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { List 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); } @@ -221,6 +223,7 @@ private StudentParticipation createNewParticipation(Exercise exercise, Participa participation.setInitializationState(InitializationState.UNINITIALIZED); participation.setExercise(exercise); participation.setParticipant(participant); + participation.setNumberOfAttempts(0); return studentParticipationRepository.saveAndFlush(participation); } @@ -638,11 +641,12 @@ public Optional findOneByExerciseAndStudentLoginWithEagerS Optional optionalTeam = teamRepository.findOneByExerciseIdAndUserLogin(exercise.getId(), username); return optionalTeam.flatMap(team -> studentParticipationRepository.findWithEagerLegalSubmissionsAndTeamStudentsByExerciseIdAndTeamId(exercise.getId(), team.getId())); } + // If exercise is a test exam exercise we load the last participation, since there are multiple participations + if (exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { + return studentParticipationRepository.findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); + } return studentParticipationRepository.findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); - } - public Optional findLatestByExerciseAndStudentLoginWithEagerSubmissionsAnyState(Exercise exercise, String username) { - return studentParticipationRepository.findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); } /** From bf79e1b6295a5a8146622c0307150cd23d6e3f22 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 13:52:53 +0200 Subject: [PATCH 20/73] check for the testExam in the live-events --- .../artemis/web/rest/StudentExamResource.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java index 73133896d839..ba826f238fb4 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java @@ -6,7 +6,6 @@ import static java.time.ZonedDateTime.now; import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Objects; import java.util.Set; @@ -488,7 +487,7 @@ public ResponseEntity getStudentExamForSummary(@PathVariable Long c log.debug("REST request to get the student exam of user {} for exam {}", user.getLogin(), examId); // 1st: Get the studentExam from the database - StudentExam studentExam = studentExamRepository.findByIdWithExercisesElseThrow(studentExamId); + StudentExam studentExam = studentExamRepository.findByIdWithExercisesAndStudentParticipationsElseThrow(studentExamId); // 2nd: Check equal users and access permissions if (!user.equals(studentExam.getUser())) { @@ -563,8 +562,16 @@ public ResponseEntity> getExamLiveEvents(@PathVariable Lo User currentUser = userRepository.getUserWithGroupsAndAuthorities(); log.debug("REST request to get the exam live events for exam {} by user {}", examId, currentUser.getLogin()); - StudentExam studentExam = studentExamRepository.findByExamIdAndUserId(examId, currentUser.getId()) - .orElseThrow(() -> new EntityNotFoundException("StudentExam for exam " + examId + " and user " + currentUser.getId() + " does not exist")); + boolean testExam = examRepository.isTestExam(examId); + StudentExam studentExam; + if (testExam) { + studentExam = studentExamRepository.findLatestByExamIdAndUserId(examId, currentUser.getId()) + .orElseThrow(() -> new EntityNotFoundException("StudentExam for exam " + examId + " and user " + currentUser.getId() + " does not exist")); + } + else { + studentExam = studentExamRepository.findByExamIdAndUserId(examId, currentUser.getId()) + .orElseThrow(() -> new EntityNotFoundException("StudentExam for exam " + examId + " and user " + currentUser.getId() + " does not exist")); + } if (studentExam.isTestRun()) { throw new BadRequestAlertException("Test runs do not have live events", ENTITY_NAME, "testRunNoLiveEvents"); @@ -748,12 +755,9 @@ private void prepareStudentExamForConduction(HttpServletRequest request, User cu if (studentExam.isTestExam()) { boolean setupTestExamNeeded = studentExam.isStarted() == null || !studentExam.isStarted(); if (setupTestExamNeeded) { - // Fix startedDate. As the studentExam.startedDate is used to link the participation.initializationDate, we need to drop the ms - // (initializationDate is stored with ms) - ZonedDateTime startedDate = now().truncatedTo(ChronoUnit.SECONDS); - // Set up new participations for the Exercises and set initialisationDate to the startedDate - studentExamService.setUpTestExamExerciseParticipationsAndSubmissions(studentExam, startedDate); + // Set up new participations for the Exercises + studentExamService.setUpTestExamExerciseParticipationsAndSubmissions(studentExam); } } From 7f3da7809cc5543d900e6dfd886cb43b4ee66ed2 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 13:53:41 +0200 Subject: [PATCH 21/73] change method in the exam summary method --- .../repository/StudentExamRepository.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index 5f68ced7f447..302f29376e91 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -42,6 +42,9 @@ public interface StudentExamRepository extends JpaRepository @EntityGraph(type = LOAD, attributePaths = { "exercises" }) Optional findWithExercisesById(Long studentExamId); + @EntityGraph(type = LOAD, attributePaths = { "exercises", "studentParticipations" }) + Optional findWithExercisesAndStudentParticipationsById(Long studentExamId); + @Query(""" SELECT se FROM StudentExam se @@ -215,6 +218,22 @@ SELECT COUNT(se) """) Optional findByExamIdAndUserId(@Param("examId") long examId, @Param("userId") long userId); + @Query(""" + SELECT DISTINCT se + FROM StudentExam se + WHERE se.testRun = FALSE + AND se.exam.id = :examId + AND se.user.id = :userId + AND se.id = ( + SELECT MAX(se2.id) + FROM StudentExam se2 + WHERE se2.testRun = FALSE + AND se2.exam.id = :examId + AND se2.user.id = :userId + ) + """) + Optional findLatestByExamIdAndUserId(@Param("examId") long examId, @Param("userId") long userId); + /** * Checks if any StudentExam exists for the given user (student) id in the given course. * @@ -345,6 +364,11 @@ 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)); + } + /** * Get one student exam by id with exercises, programming exercise submission policy and sessions * From f6320532bc6dfbd1cb57acad2b36418b3489a749 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 13:56:04 +0200 Subject: [PATCH 22/73] filter out participations in the ExamService --- .../de/tum/in/www1/artemis/service/exam/ExamService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java index c52509df2c8d..30867e356e27 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java @@ -561,6 +561,13 @@ public void fetchParticipationsSubmissionsAndResultsForExam(StudentExam studentE public void filterParticipationForExercise(StudentExam studentExam, Exercise exercise, List participations, boolean isAtLeastInstructor) { // remove the unnecessary inner course attribute exercise.setCourse(null); + + // If test exam, filter out participations that don't belong to this student exam + if (studentExam.isTestExam()) { + List ids = studentExam.getStudentParticipations().stream().map(StudentParticipation::getId).toList(); + participations = participations.stream().filter(participation -> ids.contains(participation.getId())).collect(Collectors.toList()); + } + if (!(exercise instanceof QuizExercise)) { // Note: quiz exercises are filtered below exercise.filterSensitiveInformation(); From b9cd7db0d0b50abd168c7537951f10b09136b77d Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 14:07:49 +0200 Subject: [PATCH 23/73] set number of attempts to 255 --- .../app/exam/participate/exam-participation.component.ts | 9 +++++++++ .../overview/course-exams/course-exams.component.html | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/exam/participate/exam-participation.component.ts b/src/main/webapp/app/exam/participate/exam-participation.component.ts index b8fafd29c680..c2c41e668279 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.component.ts +++ b/src/main/webapp/app/exam/participate/exam-participation.component.ts @@ -178,6 +178,15 @@ export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentC }, error: () => (this.loadingExam = false), }); + } else if (this.testExam && this.studentExamId) { + this.examParticipationService.loadStudentExamWithExercisesForSummary(this.courseId, this.examId, this.studentExamId).subscribe({ + next: (studentExam) => { + this.handleStudentExam(studentExam); + }, + error: () => { + this.handleNoStudentExam(); + }, + }); } else { this.examParticipationService.getOwnStudentExam(this.courseId, this.examId).subscribe({ next: (studentExam) => { diff --git a/src/main/webapp/app/overview/course-exams/course-exams.component.html b/src/main/webapp/app/overview/course-exams/course-exams.component.html index 746f59e75b1d..fc02eed0401f 100644 --- a/src/main/webapp/app/overview/course-exams/course-exams.component.html +++ b/src/main/webapp/app/overview/course-exams/course-exams.component.html @@ -28,7 +28,7 @@
Test Exams
From 3a022b5dd8e03be160ad2bd09492aff4f40887c6 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 15:37:17 +0200 Subject: [PATCH 24/73] add comment --- .../service/ParticipationServiceTest.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java index 34af5ff620f5..2b8633a413b3 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java @@ -119,22 +119,22 @@ void testCreateParticipationForExternalSubmission() throws Exception { assertThat(programmingSubmission.getResults()).isNullOrEmpty(); // results are not added in the invoked method above } - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testStartExerciseWithInitializationDate_newParticipation() { - Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - Exercise modelling = course.getExercises().iterator().next(); - Participant participant = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); - ZonedDateTime initializationDate = ZonedDateTime.now().minusHours(5); - - StudentParticipation studentParticipationReceived = participationService.startExerciseWithInitializationDate(modelling, participant, true, initializationDate); - - assertThat(studentParticipationReceived.getExercise()).isEqualTo(modelling); - assertThat(studentParticipationReceived.getStudent()).isPresent(); - assertThat(studentParticipationReceived.getStudent().get()).isEqualTo(participant); - assertThat(studentParticipationReceived.getInitializationDate()).isEqualTo(initializationDate); - assertThat(studentParticipationReceived.getInitializationState()).isEqualTo(InitializationState.INITIALIZED); - } + // @Test + // @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + // void testStartExercise_newParticipation() { + // Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); + // Exercise modelling = course.getExercises().iterator().next(); + // Participant participant = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + // ZonedDateTime initializationDate = ZonedDateTime.now().minusHours(5); + // + // StudentParticipation studentParticipationReceived = participationService.startExercise(modelling, participant, true); + // + // assertThat(studentParticipationReceived.getExercise()).isEqualTo(modelling); + // assertThat(studentParticipationReceived.getStudent()).isPresent(); + // assertThat(studentParticipationReceived.getStudent().get()).isEqualTo(participant); + // assertThat(studentParticipationReceived.getInitializationDate()).isEqualTo(initializationDate); + // assertThat(studentParticipationReceived.getInitializationState()).isEqualTo(InitializationState.INITIALIZED); + // } @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") From 0023448339c0a4a1f433850966935d3fc6ee440d Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 15:38:37 +0200 Subject: [PATCH 25/73] adjust comments --- .../de/tum/in/www1/artemis/service/exam/ExamAccessService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java index 77db0a2f8114..812d8b00c23d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java @@ -86,7 +86,7 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { // For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource studentExam.setExercises(null); } - // TODO Michal Kawka is it possible, that the list has more elements. If yes, throw an error? + // TODO Michal Kawka I think we should throw an error if the list has more than one element, since it's a violation else { studentExam = unfinishedStudentExams.getFirst(); } @@ -103,6 +103,7 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { } else { // We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam + // TODO Michal Kawka I think we can throw entity not found there throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME, "StudentExamGenerationOnlyForTestExams", true); } From 0fb3cfdb755ca9ec2831d50574e051f9d9d4e38b Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 16 May 2024 16:29:57 +0200 Subject: [PATCH 26/73] remove commented method --- .../service/exam/ExamAccessService.java | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java index 812d8b00c23d..dfe780eb707b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java @@ -125,59 +125,6 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { return studentExam; } - // public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { - // User currentUser = userRepository.getUserWithGroupsAndAuthorities(); - // - // // TODO: we should distinguish the whole method between test exam and real exam to improve the readability of the code - // // Check that the current user is at least student in the course. - // Course course = courseRepository.findByIdElseThrow(courseId); - // authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, currentUser); - // Exam exam = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examId); - // StudentExam studentExam; - // - // if (exam.isTestExam()) { - // List unsubmittedStudentExams = studentExamRepository.findUnsubmittedStudentExamsForTestExamsByExamIdAndUserId(examId, currentUser.getId()); - // if (unsubmittedStudentExams.isEmpty()) { - // studentExam = studentExamService.generateTestExam(exam, currentUser); - // // For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource - // studentExam.setExercises(null); - // } - // else { - // studentExam = unsubmittedStudentExams.getFirst(); - // } - // // Check that the current user is registered for the test exam. Otherwise, the student can self-register - // examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course, exam.getId(), currentUser); - // - // } - // else { - // // Check that the student exam exists - // Optional optionalStudentExam = studentExamRepository.findByExamIdAndUserId(examId, currentUser.getId()); - // if (optionalStudentExam.isPresent()) { - // studentExam = optionalStudentExam.get(); - // } - // else { - // // We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam - // throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME, - // "StudentExamGenerationOnlyForTestExams", true); - // } - // } - // - // checkExamBelongsToCourseElseThrow(courseId, exam); - // - // if (!examId.equals(exam.getId())) { - // throw new BadRequestAlertException("The provided examId does not match with the examId of the studentExam", ENTITY_NAME, "examIdMismatch"); - // } - // - // // Check that the exam is visible - // if (exam.getVisibleDate() != null && exam.getVisibleDate().isAfter(ZonedDateTime.now())) { - // throw new AccessForbiddenException(ENTITY_NAME, examId); - // } - // - // // NOTE: the check examRepository.isUserRegisteredForExam is not necessary because we already checked before that there is a student exam in this case for the current user - // - // return studentExam; - // } - /** * Checks if the current user is allowed to manage exams of the given course. * From 7d4a49c540962ffe15d643b9f5bdd3d2529a6c7c Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 28 May 2024 08:42:27 +0200 Subject: [PATCH 27/73] fetch latest student exam in ResultService --- src/main/java/de/tum/in/www1/artemis/service/ResultService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java index 3ec13cb3d5e0..b493518d7b9d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java @@ -325,7 +325,7 @@ private void filterSensitiveFeedbacksInExamExercise(Participation participation, boolean shouldResultsBePublished = exam.resultsPublished(); if (!shouldResultsBePublished && exam.isTestExam() && participation instanceof StudentParticipation studentParticipation) { var participant = studentParticipation.getParticipant(); - var studentExamOptional = studentExamRepository.findByExamIdAndUserId(exam.getId(), participant.getId()); + var studentExamOptional = studentExamRepository.findLatestByExamIdAndUserId(exam.getId(), participant.getId()); if (studentExamOptional.isPresent()) { shouldResultsBePublished = studentExamOptional.get().areResultsPublishedYet(); } From 0fbb9c39c59b66a1a0e92ebb2b340f3761a41fdc Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 28 May 2024 08:43:12 +0200 Subject: [PATCH 28/73] add repository method to fetch latest StudentParticipation --- ...ammingExerciseStudentParticipationRepository.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java index 99ad9010b667..ddb0e8dbfa9c 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -156,6 +156,18 @@ List findWithSubmissionsByExerciseIdAnd Optional 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 + AND participation.id = (SELECT MAX(p2.id) FROM StudentParticipation p2 WHERE p2.exercise.id = :exerciseId AND p2.student.login = :username) + """) + Optional findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, + @Param("username") String username, @Param("testRun") boolean testRun); + @Query(""" SELECT participation FROM ProgrammingExerciseStudentParticipation participation From 1a8c1a52442a25e004a9e137ff39daf0cb9bf7c8 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 28 May 2024 08:43:49 +0200 Subject: [PATCH 29/73] distinguish between test exam and other exam types in ProgrammingExerciseParticipationService --- ...ProgrammingExerciseParticipationService.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java index d40a546decc5..ab11b54e9dd0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java @@ -170,11 +170,22 @@ public ProgrammingExerciseStudentParticipation findStudentParticipationByExercis Optional participationOptional; - if (withSubmissions) { - participationOptional = studentParticipationRepository.findWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun); + if (exercise.isExamExercise() && exercise.getExerciseGroup().getExam().isTestExam()) { + if (withSubmissions) { + participationOptional = studentParticipationRepository.findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun); + } + else { + // TODO Michal Kawka without submissions + participationOptional = studentParticipationRepository.findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(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); + } } if (participationOptional.isEmpty()) { From 7fc87588715a9dbd3e2729a43af5a8d40de5862c Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 28 May 2024 08:44:09 +0200 Subject: [PATCH 30/73] change ParticipationServiceTest --- .../tum/in/www1/artemis/service/ParticipationServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java index 40b85eb10689..5eaf6bc70b8a 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java @@ -156,7 +156,7 @@ void testStartExerciseWithInitializationDate_newParticipation() { Participant participant = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); ZonedDateTime initializationDate = ZonedDateTime.now().minusHours(5); - StudentParticipation studentParticipationReceived = participationService.startExerciseWithInitializationDate(modelling, participant, true, initializationDate); + StudentParticipation studentParticipationReceived = participationService.startExercise(modelling, participant, true); assertThat(studentParticipationReceived.getExercise()).isEqualTo(modelling); assertThat(studentParticipationReceived.getStudent()).isPresent(); From aaaa4b24d61966f5a201b31f271d1c9c34e70d41 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 29 May 2024 08:43:43 +0200 Subject: [PATCH 31/73] fetch latest participation for the test exam exercise --- .../tum/in/www1/artemis/service/ParticipationService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index c97bdcc46103..34dd0d70ce5d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -567,6 +567,12 @@ public Optional findOneByExerciseAndStudentLoginAnyState(E Optional optionalTeam = teamRepository.findOneByExerciseIdAndUserLogin(exercise.getId(), username); return optionalTeam.flatMap(team -> studentParticipationRepository.findOneByExerciseIdAndTeamId(exercise.getId(), team.getId())); } + + if (exercise.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { + // TODO Michal Kawka without submission + return studentParticipationRepository.findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); + } + return studentParticipationRepository.findByExerciseIdAndStudentLogin(exercise.getId(), username); } From 87b932134b617ae1fe5ed95370cc21ec435d6bf0 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 29 May 2024 16:45:19 +0200 Subject: [PATCH 32/73] implement findLatest methods --- ...grammingExerciseStudentParticipationRepository.java | 10 ++++++++++ .../repository/StudentParticipationRepository.java | 9 +++++++++ .../in/www1/artemis/service/ParticipationService.java | 3 +-- .../www1/artemis/service/exam/ExamAccessService.java | 7 +++++-- .../ProgrammingExerciseParticipationService.java | 3 +-- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java index ddb0e8dbfa9c..ee1f51a7ae37 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -91,6 +91,16 @@ default ProgrammingExerciseStudentParticipation findWithSubmissionsByExerciseIdA Optional 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 + AND participation.id = (SELECT MAX(p2.id) FROM StudentParticipation p2 WHERE p2.exercise.id = :exerciseId AND p2.student.login = :username) + """) + Optional findLatestByExerciseIdAndStudentLoginAndTestRun(long exerciseId, String username, boolean testRun); + @EntityGraph(type = LOAD, attributePaths = { "team.students" }) Optional findByExerciseIdAndTeamId(long exerciseId, long teamId); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 3077ca0efd72..fd1963c8dd66 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -115,6 +115,15 @@ SELECT COUNT(p.id) > 0 """) Optional 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 + AND p.id = (SELECT MAX(p2.id) FROM StudentParticipation p2 WHERE p2.exercise.id = :exerciseId AND p2.student.login = :username) + """) + Optional findLatestByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username); + @Query(""" SELECT DISTINCT p FROM StudentParticipation p diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index 34dd0d70ce5d..e2477a9a78d0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -569,8 +569,7 @@ public Optional findOneByExerciseAndStudentLoginAnyState(E } if (exercise.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { - // TODO Michal Kawka without submission - return studentParticipationRepository.findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); + return studentParticipationRepository.findLatestByExerciseIdAndStudentLogin(exercise.getId(), username); } return studentParticipationRepository.findByExerciseIdAndStudentLogin(exercise.getId(), username); diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java index dfe780eb707b..1ad2555ce57e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java @@ -86,10 +86,13 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { // For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource studentExam.setExercises(null); } - // TODO Michal Kawka I think we should throw an error if the list has more than one element, since it's a violation - else { + else if (unfinishedStudentExams.size() == 1) { studentExam = unfinishedStudentExams.getFirst(); } + else { + throw new IllegalStateException( + "User " + currentUser.getId() + " has " + unfinishedStudentExams.size() + " unfinished test exams for exam " + examId + " in course " + courseId); + } // Check that the current user is registered for the test exam. Otherwise, the student can self-register examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course, exam.getId(), currentUser); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java index ab11b54e9dd0..4260bd8270b3 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java @@ -175,8 +175,7 @@ public ProgrammingExerciseStudentParticipation findStudentParticipationByExercis participationOptional = studentParticipationRepository.findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun); } else { - // TODO Michal Kawka without submissions - participationOptional = studentParticipationRepository.findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun); + participationOptional = studentParticipationRepository.findLatestByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun); } } else { From 38e08bf7e3b08fb23583442cec755fac785cfe2d Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 3 Jun 2024 08:44:13 +0200 Subject: [PATCH 33/73] remove attemptNumber from repository slug --- .../in/www1/artemis/service/ParticipationService.java | 5 ++++- .../connectors/localvc/LocalVCRepositoryUri.java | 3 ++- .../connectors/vcs/AbstractVersionControlService.java | 10 +++++++--- .../service/connectors/vcs/VersionControlService.java | 4 ++-- .../programming/ProgrammingExerciseImportService.java | 8 ++++---- .../participation/ParticipationUtilService.java | 5 +++-- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index e2477a9a78d0..0a508b8ca074 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -266,6 +266,9 @@ private StudentParticipation startPracticeMode(ProgrammingExercise exercise, Pro participation = copyRepository(exercise, exercise.getVcsTemplateRepositoryUri(), participation); } + // For practice mode 1 is always set. For more information see Participation.class + participation.setNumberOfAttempts(1); + return startProgrammingParticipation(exercise, participation, setInitializationDate); } @@ -451,7 +454,7 @@ private ProgrammingExerciseStudentParticipation copyRepository(ProgrammingExerci 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()); // add the userInfo part to the repoUri only if the participation belongs to a single student (and not a team of students) if (participation.getStudent().isPresent()) { newRepoUri = newRepoUri.withUser(participation.getParticipantIdentifier()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUri.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUri.java index a22079cc7fe8..882a5cfb810c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUri.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCRepositoryUri.java @@ -208,7 +208,8 @@ private String buildRepositoryPath(String projectKey, String repositorySlug) { * @return The normalized repository type or username, free of the project key prefix and "practice-" designation. */ private String getRepositoryTypeOrUserName(String repositorySlug, String projectKey) { - String repositoryTypeOrUserNameWithPracticePrefix = repositorySlug.toLowerCase().replace(projectKey.toLowerCase() + "-", ""); + String pattern = projectKey.toLowerCase() + "\\d*-"; + String repositoryTypeOrUserNameWithPracticePrefix = repositorySlug.toLowerCase().replaceAll(pattern, ""); return repositoryTypeOrUserNameWithPracticePrefix.replace("practice-", ""); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java index fcb98a759d79..f355c369bbbc 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java @@ -94,11 +94,15 @@ public void addWebHookForParticipation(ProgrammingExerciseParticipation particip } @Override - public VcsRepositoryUri copyRepository(String sourceProjectKey, String sourceRepositoryName, String sourceBranch, String targetProjectKey, String targetRepositoryName) - throws VersionControlException { + public VcsRepositoryUri copyRepository(String sourceProjectKey, String sourceRepositoryName, String sourceBranch, String targetProjectKey, String targetRepositoryName, + Integer numberOfAttempts) throws VersionControlException { sourceRepositoryName = sourceRepositoryName.toLowerCase(); targetRepositoryName = targetRepositoryName.toLowerCase(); - final String targetRepoSlug = targetProjectKey.toLowerCase() + "-" + targetRepositoryName; + String targetProjectKeyLowerCase = targetProjectKey.toLowerCase(); + if (numberOfAttempts != null && numberOfAttempts > 0) { + targetProjectKeyLowerCase = targetProjectKeyLowerCase + numberOfAttempts; + } + final String targetRepoSlug = targetProjectKeyLowerCase + "-" + targetRepositoryName; // get the remote url of the source repo final var sourceRepoUri = getCloneRepositoryUri(sourceProjectKey, sourceRepositoryName); // get the remote url of the target repo diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java index a6d867d67fea..1714da10d72a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java @@ -128,8 +128,8 @@ public interface VersionControlService { * @return The URL for cloning the repository * @throws VersionControlException if the repository could not be copied on the VCS server (e.g. because the source repo does not exist) */ - VcsRepositoryUri copyRepository(String sourceProjectKey, String sourceRepositoryName, String sourceBranch, String targetProjectKey, String targetRepositoryName) - throws VersionControlException; + VcsRepositoryUri copyRepository(String sourceProjectKey, String sourceRepositoryName, String sourceBranch, String targetProjectKey, String targetRepositoryName, + Integer numberOfAttempts) throws VersionControlException; /** * Add the user to the repository diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java index 135661c2cf82..5d5392346b76 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java @@ -114,14 +114,14 @@ public void importRepositories(final ProgrammingExercise templateExercise, final String sourceBranch = versionControl.getOrRetrieveBranchOfExercise(templateExercise); // TODO: in case one of those operations fail, we should do error handling and revert all previous operations - versionControl.copyRepository(sourceProjectKey, templateRepoName, sourceBranch, targetProjectKey, RepositoryType.TEMPLATE.getName()); - versionControl.copyRepository(sourceProjectKey, solutionRepoName, sourceBranch, targetProjectKey, RepositoryType.SOLUTION.getName()); - versionControl.copyRepository(sourceProjectKey, testRepoName, sourceBranch, targetProjectKey, RepositoryType.TESTS.getName()); + versionControl.copyRepository(sourceProjectKey, templateRepoName, sourceBranch, targetProjectKey, RepositoryType.TEMPLATE.getName(), null); + versionControl.copyRepository(sourceProjectKey, solutionRepoName, sourceBranch, targetProjectKey, RepositoryType.SOLUTION.getName(), null); + versionControl.copyRepository(sourceProjectKey, testRepoName, sourceBranch, targetProjectKey, RepositoryType.TESTS.getName(), null); List auxRepos = templateExercise.getAuxiliaryRepositories(); for (int i = 0; i < auxRepos.size(); i++) { AuxiliaryRepository auxRepo = auxRepos.get(i); - var repoUri = versionControl.copyRepository(sourceProjectKey, auxRepo.getRepositoryName(), sourceBranch, targetProjectKey, auxRepo.getName()).toString(); + var repoUri = versionControl.copyRepository(sourceProjectKey, auxRepo.getRepositoryName(), sourceBranch, targetProjectKey, auxRepo.getName(), null).toString(); AuxiliaryRepository newAuxRepo = newExercise.getAuxiliaryRepositories().get(i); newAuxRepo.setRepositoryUri(repoUri); auxiliaryRepositoryRepository.save(newAuxRepo); diff --git a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationUtilService.java b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationUtilService.java index 3cbcfe913aea..35eb23bc6714 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationUtilService.java @@ -890,7 +890,8 @@ public void mockCreationOfExerciseParticipation(boolean useGradedParticipationOf public void mockCreationOfExerciseParticipation(String templateRepoName, ProgrammingExercise programmingExercise, VersionControlService versionControlService, ContinuousIntegrationService continuousIntegrationService) throws URISyntaxException { var someURL = new VcsRepositoryUri("http://vcs.fake.fake"); - doReturn(someURL).when(versionControlService).copyRepository(any(String.class), eq(templateRepoName), any(String.class), any(String.class), any(String.class)); + doReturn(someURL).when(versionControlService).copyRepository(any(String.class), eq(templateRepoName), any(String.class), any(String.class), any(String.class), + any(Integer.class)); mockCreationOfExerciseParticipationInternal(programmingExercise, versionControlService, continuousIntegrationService); } @@ -905,7 +906,7 @@ public void mockCreationOfExerciseParticipation(String templateRepoName, Program public void mockCreationOfExerciseParticipation(ProgrammingExercise programmingExercise, VersionControlService versionControlService, ContinuousIntegrationService continuousIntegrationService) throws URISyntaxException { var someURL = new VcsRepositoryUri("http://vcs.fake.fake"); - doReturn(someURL).when(versionControlService).copyRepository(any(String.class), any(), any(String.class), any(String.class), any(String.class)); + doReturn(someURL).when(versionControlService).copyRepository(any(String.class), any(), any(String.class), any(String.class), any(String.class), any(Integer.class)); mockCreationOfExerciseParticipationInternal(programmingExercise, versionControlService, continuousIntegrationService); } From dc957072c6c5de9494598c71e804fc28153de793 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 5 Jun 2024 14:17:49 +0200 Subject: [PATCH 34/73] remove redundant call to set attempt number --- .../de/tum/in/www1/artemis/service/ParticipationService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index 0a508b8ca074..3690b949c962 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -223,7 +223,6 @@ private StudentParticipation createNewParticipation(Exercise exercise, Participa participation.setInitializationState(InitializationState.UNINITIALIZED); participation.setExercise(exercise); participation.setParticipant(participant); - participation.setNumberOfAttempts(0); return studentParticipationRepository.saveAndFlush(participation); } From 564bde921a7c7dd1022000b54a46c5642747548a Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 5 Jun 2024 14:18:15 +0200 Subject: [PATCH 35/73] convert list to a set to improve performance --- .../java/de/tum/in/www1/artemis/service/exam/ExamService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java index 79d53a88dd22..0ef0a22cb257 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java @@ -564,7 +564,7 @@ public void filterParticipationForExercise(StudentExam studentExam, Exercise exe // If test exam, filter out participations that don't belong to this student exam if (studentExam.isTestExam()) { - List ids = studentExam.getStudentParticipations().stream().map(StudentParticipation::getId).toList(); + Set ids = studentExam.getStudentParticipations().stream().map(StudentParticipation::getId).collect(Collectors.toSet()); participations = participations.stream().filter(participation -> ids.contains(participation.getId())).collect(Collectors.toList()); } From ca54780548ba98edbd8a5ee13c5b2303b0bd89d5 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 5 Jun 2024 14:18:45 +0200 Subject: [PATCH 36/73] add attempt number to repository name --- .../service/connectors/vcs/AbstractVersionControlService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java index f355c369bbbc..bde2c602c260 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/AbstractVersionControlService.java @@ -99,7 +99,7 @@ public VcsRepositoryUri copyRepository(String sourceProjectKey, String sourceRep sourceRepositoryName = sourceRepositoryName.toLowerCase(); targetRepositoryName = targetRepositoryName.toLowerCase(); String targetProjectKeyLowerCase = targetProjectKey.toLowerCase(); - if (numberOfAttempts != null && numberOfAttempts > 0) { + if (numberOfAttempts != null && numberOfAttempts > 0 && !targetRepositoryName.contains("practice-")) { targetProjectKeyLowerCase = targetProjectKeyLowerCase + numberOfAttempts; } final String targetRepoSlug = targetProjectKeyLowerCase + "-" + targetRepositoryName; From da998a012245aa76e949af5be330e3d878a67c77 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 09:09:26 +0200 Subject: [PATCH 37/73] rename method and remove submission policy --- .../www1/artemis/repository/StudentExamRepository.java | 9 ++++----- .../in/www1/artemis/web/rest/StudentExamResource.java | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index c6fac080e95d..01aa624c6e73 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -49,11 +49,10 @@ public interface StudentExamRepository extends JpaRepository SELECT se FROM StudentExam se LEFT JOIN FETCH se.exercises e - LEFT JOIN FETCH e.submissionPolicy LEFT JOIN FETCH se.examSessions WHERE se.id = :studentExamId """) - Optional findWithExercisesSubmissionPolicyAndSessionsById(@Param("studentExamId") long studentExamId); + Optional findWithExercisesAndSessionsById(@Param("studentExamId") long studentExamId); @Query(""" SELECT DISTINCT se @@ -370,14 +369,14 @@ default StudentExam findByIdWithExercisesAndStudentParticipationsElseThrow(Long } /** - * 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)); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java index a144ac85ca5b..b5b4930bac22 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java @@ -178,7 +178,7 @@ public ResponseEntity getStudentExam(@PathVariable Long examAccessService.checkCourseAndExamAndStudentExamAccessElseThrow(courseId, examId, studentExamId); - StudentExam studentExam = studentExamRepository.findByIdWithExercisesSubmissionPolicyAndSessionsElseThrow(studentExamId); + StudentExam studentExam = studentExamRepository.findByIdWithExercisesAndSessionsElseThrow(studentExamId); examService.loadQuizExercisesForStudentExam(studentExam); From 044fadc138764aeb15e431e1ca600d14df8b7fb8 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 09:10:00 +0200 Subject: [PATCH 38/73] change router link --- .../programming-exam-summary.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html b/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html index df611ba4e49a..66f71e05bbae 100644 --- a/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html +++ b/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html @@ -9,7 +9,7 @@
{{ 'artemisApp.exam.examSummary.yourSubmission' | artemisTranslate }}
From 7e7b868de906f5bf70e066e13d8d284307dd8c46 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 09:11:14 +0200 Subject: [PATCH 39/73] change logic of openStudentExam --- ...ourse-exam-attempt-review-detail.component.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts index 80df750bebe6..50ee172cf117 100644 --- a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts +++ b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts @@ -85,16 +85,22 @@ export class CourseExamAttemptReviewDetailComponent implements OnInit, OnDestroy } /** - * navigate to /courses/:courseId/exams/:examId/test-exam/:studentExamId if the attempt is either submitted or the time is up + * do nothing if student didn't submit on time + * navigate to /courses/:courseId/exams/:examId/test-exam/:studentExamId if the attempt submitted * navigate to /courses/:courseId/exams/:examId/test-exam/start if the attempt can be continued * Used to open the corresponding studentExam */ openStudentExam(): void { - // If exam is submitted or the time us up, navigate to the exam overview - // else navigate to the exam start page to continue the attempt - if (this.studentExam.submitted || !this.withinWorkingTime) { + // If student didn't submit on time, there's no exam overview + if (!this.withinWorkingTime && !this.studentExam.submitted) { + return; + } + // If exam is submitted navigate to the exam overview + else if (this.studentExam.submitted) { this.router.navigate(['courses', this.courseId, 'exams', this.exam.id, 'test-exam', this.studentExam.id]); /// - } else { + } + // If exam is not submitted and within working time, resume attempt + else if (this.withinWorkingTime) { this.router.navigate(['courses', this.courseId, 'exams', this.exam.id, 'test-exam', 'start']); } } From ae91e10e68175bb0b33f6a8c22df2c6327780d21 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 09:13:10 +0200 Subject: [PATCH 40/73] make attempt component non-clickable if the attempt was not submitted on time --- ...-exam-attempt-review-detail.component.html | 115 +++++++++--------- 1 file changed, 57 insertions(+), 58 deletions(-) diff --git a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html index fba3445ad45e..56157ec9dc83 100644 --- a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html +++ b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html @@ -1,62 +1,61 @@ @if (studentExam) { -
- -
-
- -

- @if (withinWorkingTime) { - - } - @if (!withinWorkingTime && studentExam.submitted) { - - } - @if (!withinWorkingTime && !studentExam.submitted) { - - } -

-
-
-
-
- {{ 'artemisApp.exam.overview.testExam.' + (withinWorkingTime ? 'resumeAttempt' : 'reviewAttempt') | artemisTranslate: { attempt: index } }} -
-
-
- @if (withinWorkingTime) { -
- {{ 'artemisApp.exam.overview.testExam.workingTimeLeft' | artemisTranslate }} {{ workingTimeLeftInSeconds() | artemisDurationFromSeconds: true }} -
- } - @if (studentExam.submitted) { -
- @if (studentExam.submissionDate) { -
- {{ 'artemisApp.exam.overview.testExam.submissionDate' | artemisTranslate }} {{ studentExam.submissionDate | artemisDate }} -
- } - @if (studentExam.submissionDate && studentExam.startedDate) { -
- {{ 'artemisApp.exam.overview.testExam.workingTimeCalculated' | artemisTranslate }} - -
- } -
- } - - @if (!withinWorkingTime && !studentExam.submitted) { -
-
{{ 'artemisApp.exam.overview.testExam.notSubmitted' | artemisTranslate }}
-
- } -
+ +
+
+ +

+ @if (withinWorkingTime) { + + } + @if (!withinWorkingTime && studentExam.submitted) { + + } + @if (!withinWorkingTime && !studentExam.submitted) { + + } +

+
+
+
+
+ {{ 'artemisApp.exam.overview.testExam.' + (withinWorkingTime ? 'resumeAttempt' : 'reviewAttempt') | artemisTranslate: { attempt: index } }} +
+
+
+ @if (withinWorkingTime) { +
+ {{ 'artemisApp.exam.overview.testExam.workingTimeLeft' | artemisTranslate }} {{ workingTimeLeftInSeconds() | artemisDurationFromSeconds: true }} +
+ } + @if (studentExam.submitted) { +
+ @if (studentExam.submissionDate) { +
+ {{ 'artemisApp.exam.overview.testExam.submissionDate' | artemisTranslate }} {{ studentExam.submissionDate | artemisDate }} +
+ } + @if (studentExam.submissionDate && studentExam.startedDate) { +
+ {{ 'artemisApp.exam.overview.testExam.workingTimeCalculated' | artemisTranslate }} + +
+ } +
+ } + + @if (!withinWorkingTime && !studentExam.submitted) { +
+
{{ 'artemisApp.exam.overview.testExam.notSubmitted' | artemisTranslate }}
+
+ }
From 5f345731e4a534ef43ffe1e7b70d3c55ddcfb50a Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 10:52:39 +0200 Subject: [PATCH 41/73] change repositoryLink for test-exams --- .../exercise-details-student-actions.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts index 74fcd9198a68..4e27b745f3ca 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts @@ -83,6 +83,9 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges this.repositoryLink += `/${this.exercise.id}`; } if (this.repositoryLink.includes('exams')) { + if (this.repositoryLink.includes('test-exam')) { + this.repositoryLink = this.repositoryLink.replace('/test-exam/start', ''); + } this.repositoryLink += `/exercises/${this.exercise.id}`; } if (this.exercise.type === ExerciseType.QUIZ) { From 49f5d11d954405e0c03087f6ff2e007b64e5233c Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 11:09:37 +0200 Subject: [PATCH 42/73] remove @Transactional --- .../tum/in/www1/artemis/service/exam/StudentExamService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java index bac8bfe6f539..2b9da23f367f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java @@ -31,7 +31,6 @@ import org.springframework.context.annotation.Profile; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.FileUploadExercise; @@ -819,7 +818,6 @@ private StudentExam generateIndividualStudentExam(Exam exam, User student) { * @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded * @return the list of student exams with their corresponding users */ - @Transactional // TODO: NOT OK --> remove @Transactional public List generateStudentExams(final Exam exam) { final var existingStudentExams = studentExamRepository.findByExamId(exam.getId()); // deleteInBatch does not work, because it does not cascade the deletion of existing exam sessions, therefore use deleteAll @@ -840,7 +838,6 @@ public List generateStudentExams(final Exam exam) { * @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded * @return the list of student exams with their corresponding users */ - @Transactional // TODO: NOT OK --> remove @Transactional public List generateMissingStudentExams(Exam exam) { // Get all users who already have an individual exam From 1e1c3b5a5df3c98b2d7299de0845ce7b19e9cd3a Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 11:10:10 +0200 Subject: [PATCH 43/73] add @Param annotations --- .../ProgrammingExerciseStudentParticipationRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java index ee1f51a7ae37..d1554f69b079 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -99,7 +99,8 @@ default ProgrammingExerciseStudentParticipation findWithSubmissionsByExerciseIdA AND participation.testRun = :testRun AND participation.id = (SELECT MAX(p2.id) FROM StudentParticipation p2 WHERE p2.exercise.id = :exerciseId AND p2.student.login = :username) """) - Optional findLatestByExerciseIdAndStudentLoginAndTestRun(long exerciseId, String username, boolean testRun); + Optional findLatestByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username, + @Param("testRun") boolean testRun); @EntityGraph(type = LOAD, attributePaths = { "team.students" }) Optional findByExerciseIdAndTeamId(long exerciseId, long teamId); From ed10000dae34110bd866111cf0823c142fecc2cf Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 11:46:51 +0200 Subject: [PATCH 44/73] optimize query by using limit and order instead of select max --- .../artemis/repository/StudentExamRepository.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index 01aa624c6e73..365473d077ca 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -218,18 +218,13 @@ SELECT COUNT(se) Optional findByExamIdAndUserId(@Param("examId") long examId, @Param("userId") long userId); @Query(""" - SELECT DISTINCT se + SELECT se FROM StudentExam se WHERE se.testRun = FALSE AND se.exam.id = :examId AND se.user.id = :userId - AND se.id = ( - SELECT MAX(se2.id) - FROM StudentExam se2 - WHERE se2.testRun = FALSE - AND se2.exam.id = :examId - AND se2.user.id = :userId - ) + ORDER BY se.id DESC + LIMIT 1 """) Optional findLatestByExamIdAndUserId(@Param("examId") long examId, @Param("userId") long userId); From b604bf01a61e11511998dd82c2d96b18e01b4b11 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 15:02:47 +0200 Subject: [PATCH 45/73] use toList() instead of Collectors --- .../java/de/tum/in/www1/artemis/service/exam/ExamService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java index 0ef0a22cb257..e99372b4241f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java @@ -565,7 +565,7 @@ public void filterParticipationForExercise(StudentExam studentExam, Exercise exe // If test exam, filter out participations that don't belong to this student exam if (studentExam.isTestExam()) { Set ids = studentExam.getStudentParticipations().stream().map(StudentParticipation::getId).collect(Collectors.toSet()); - participations = participations.stream().filter(participation -> ids.contains(participation.getId())).collect(Collectors.toList()); + participations = participations.stream().filter(participation -> ids.contains(participation.getId())).toList(); } if (!(exercise instanceof QuizExercise)) { From 52f1e5a3ed5a7ee6365db71d796f843131716ae2 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 7 Jun 2024 15:04:44 +0200 Subject: [PATCH 46/73] change repository method --- .../tum/in/www1/artemis/repository/StudentExamRepository.java | 1 + .../tum/in/www1/artemis/service/exam/StudentExamService.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index 365473d077ca..0b01c781df0b 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -50,6 +50,7 @@ public interface StudentExamRepository extends JpaRepository FROM StudentExam se LEFT JOIN FETCH se.exercises e LEFT JOIN FETCH se.examSessions + LEFT JOIN FETCH se.studentParticipations WHERE se.id = :studentExamId """) Optional findWithExercisesAndSessionsById(@Param("studentExamId") long studentExamId); diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java index 2b9da23f367f..247f7411b700 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java @@ -639,8 +639,6 @@ public void setUpTestExamExerciseParticipationsAndSubmissions(StudentExam studen List generatedParticipations = Collections.synchronizedList(new ArrayList<>()); setUpExerciseParticipationsAndSubmissions(studentExam, generatedParticipations); // TODO: Michael Allgaier: schedule a lock operation for all involved student repositories of this student exam (test exam) at the end of the individual working time - studentExam.setStudentParticipations(generatedParticipations); - studentExamRepository.save(studentExam); studentParticipationRepository.saveAll(generatedParticipations); } @@ -693,6 +691,8 @@ public void setUpExerciseParticipationsAndSubmissions(StudentExam studentExam, L student.getParticipantIdentifier(), ex.getMessage(), ex); } } + studentExam.setStudentParticipations(generatedParticipations); + this.studentExamRepository.save(studentExam); } } From 74599b92361b4abe3c3a8b85c0b10cbe48e8189d Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 9 Jun 2024 23:38:38 +0200 Subject: [PATCH 47/73] fix the issue, that submissions of first attempt are not saved --- .../in/www1/artemis/service/exam/ExamSubmissionService.java | 4 ++-- .../tum/in/www1/artemis/service/exam/StudentExamService.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java index 34ef5f84263d..e87eafe88a2e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java @@ -152,8 +152,8 @@ private boolean isExamTestRunSubmission(Exercise exercise, User user, Exam exam) * @return the submission. If a submission already exists for the exercise we will set the id */ public Submission preventMultipleSubmissions(Exercise exercise, Submission submission, User user) { - // Return immediately if it is not an exam submissions or if it is a programming exercise - if (!exercise.isExamExercise() || exercise instanceof ProgrammingExercise) { + // Return immediately if it is not an exam submissions or if it is a programming exercise or if it is a test exam exercise + if (!exercise.isExamExercise() || exercise instanceof ProgrammingExercise || exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { return submission; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java index 247f7411b700..f7958f498e14 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java @@ -691,9 +691,9 @@ public void setUpExerciseParticipationsAndSubmissions(StudentExam studentExam, L student.getParticipantIdentifier(), ex.getMessage(), ex); } } - studentExam.setStudentParticipations(generatedParticipations); - this.studentExamRepository.save(studentExam); } + studentExam.setStudentParticipations(generatedParticipations); + this.studentExamRepository.save(studentExam); } /** From e4ddb76e82a6846fea3187e1115adaf19683da25 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 10 Jun 2024 23:48:48 +0200 Subject: [PATCH 48/73] distinguish between test exams in repository method --- .../repository/StudentParticipationRepository.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index a3155467a601..99f3342a4de4 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -979,7 +979,7 @@ private List 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 @@ -989,6 +989,18 @@ default List findByStudentExamWithEagerSubmissionsResult(S if (studentExam.isTestRun()) { return findTestRunParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResult(studentExam.getUser().getId(), studentExam.getExercises()); } + + if (studentExam.isTestExam()) { + if (withAssessor) { + // TODO Michal Kawka + return findTestRunParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResult(studentExam.getUser().getId(), studentExam.getExercises()); + } + else { + // TODO Michal Kawka + return findTestRunParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResult(studentExam.getUser().getId(), studentExam.getExercises()); + } + } + else { if (withAssessor) { return findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(studentExam.getUser().getId(), studentExam.getExercises()); From cd7dc52cafc407a7cd66f0265d3ef3d7e683f10f Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 11 Jun 2024 00:23:17 +0200 Subject: [PATCH 49/73] move filtering of the student exam participations to the repository method --- .../StudentParticipationRepository.java | 30 ++++++++++++++++--- .../artemis/service/exam/ExamService.java | 6 ---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 99f3342a4de4..b2a21b6c6405 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -764,6 +764,30 @@ List findByStudentIdAndIndividualExercisesWithEagerSubmiss List findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(@Param("studentId") long studentId, @Param("exercises") List 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 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 findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(@Param("studentExamId") long studentExamId); + @Query(""" SELECT DISTINCT p FROM StudentParticipation p @@ -992,12 +1016,10 @@ default List findByStudentExamWithEagerSubmissionsResult(S if (studentExam.isTestExam()) { if (withAssessor) { - // TODO Michal Kawka - return findTestRunParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResult(studentExam.getUser().getId(), studentExam.getExercises()); + return findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(studentExam.getId()); } else { - // TODO Michal Kawka - return findTestRunParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResult(studentExam.getUser().getId(), studentExam.getExercises()); + return findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(studentExam.getId()); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java index e99372b4241f..40c694b9a986 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java @@ -562,12 +562,6 @@ public void filterParticipationForExercise(StudentExam studentExam, Exercise exe // remove the unnecessary inner course attribute exercise.setCourse(null); - // If test exam, filter out participations that don't belong to this student exam - if (studentExam.isTestExam()) { - Set ids = studentExam.getStudentParticipations().stream().map(StudentParticipation::getId).collect(Collectors.toSet()); - participations = participations.stream().filter(participation -> ids.contains(participation.getId())).toList(); - } - if (!(exercise instanceof QuizExercise)) { // Note: quiz exercises are filtered below exercise.filterSensitiveInformation(); From 8476dcdb1fb5844fa7f881661d7c0f958a620038 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 12 Jun 2024 15:02:14 +0200 Subject: [PATCH 50/73] fix tests --- ...grammingExerciseStudentParticipationRepository.java | 6 ++++-- .../repository/StudentParticipationRepository.java | 10 ++++++---- .../in/www1/artemis/service/ParticipationService.java | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java index d1554f69b079..b6cfd25040ec 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -97,7 +97,8 @@ default ProgrammingExerciseStudentParticipation findWithSubmissionsByExerciseIdA WHERE participation.exercise.id = :exerciseId AND participation.student.login = :username AND participation.testRun = :testRun - AND participation.id = (SELECT MAX(p2.id) FROM StudentParticipation p2 WHERE p2.exercise.id = :exerciseId AND p2.student.login = :username) + ORDER BY participation.id DESC + LIMIT 1 """) Optional findLatestByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username, @Param("testRun") boolean testRun); @@ -174,7 +175,8 @@ Optional findWithSubmissionsByExerciseI WHERE participation.exercise.id = :exerciseId AND participation.student.login = :username AND participation.testRun = :testRun - AND participation.id = (SELECT MAX(p2.id) FROM StudentParticipation p2 WHERE p2.exercise.id = :exerciseId AND p2.student.login = :username) + ORDER BY participation.id DESC + LIMIT 1 """) Optional findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username, @Param("testRun") boolean testRun); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index b2a21b6c6405..eb4a87c40bec 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -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) - """) + """) boolean existsByCourseIdAndStudentId(@Param("courseId") long courseId, @Param("studentId") long studentId); @Query(""" @@ -120,7 +120,8 @@ SELECT COUNT(p.id) > 0 FROM StudentParticipation p WHERE p.exercise.id = :exerciseId AND p.student.login = :username - AND p.id = (SELECT MAX(p2.id) FROM StudentParticipation p2 WHERE p2.exercise.id = :exerciseId AND p2.student.login = :username) + ORDER BY p.id DESC + LIMIT 1 """) Optional findLatestByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username); @@ -140,8 +141,9 @@ SELECT COUNT(p.id) > 0 LEFT JOIN FETCH p.submissions s WHERE p.exercise.id = :exerciseId AND p.student.login = :username - AND p.id = (SELECT MAX(p2.id) FROM StudentParticipation p2 WHERE p2.exercise.id = :exerciseId AND p2.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 findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username); @@ -445,7 +447,7 @@ default List findByExerciseIdWithManualResultAndFeedbacksA LEFT JOIN FETCH p.submissions WHERE p.exercise.id = :exerciseId AND p.student.id = :studentId - """) + """) List findByExerciseIdAndStudentIdWithEagerResultsAndSubmissions(@Param("exerciseId") long exerciseId, @Param("studentId") long studentId); @Query(""" diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index 6e7a470ca5d5..72884f7a1ad4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -647,7 +647,7 @@ public Optional findOneByExerciseAndStudentLoginWithEagerS return optionalTeam.flatMap(team -> studentParticipationRepository.findWithEagerLegalSubmissionsAndTeamStudentsByExerciseIdAndTeamId(exercise.getId(), team.getId())); } // If exercise is a test exam exercise we load the last participation, since there are multiple participations - if (exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { + if (exercise.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { return studentParticipationRepository.findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); } return studentParticipationRepository.findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); From d00108fed3c9de1507836441137839986dabbd38 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 12 Jun 2024 16:09:56 +0200 Subject: [PATCH 51/73] adjust architecture test --- .../www1/artemis/architecture/RepositoryArchitectureTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/architecture/RepositoryArchitectureTest.java b/src/test/java/de/tum/in/www1/artemis/architecture/RepositoryArchitectureTest.java index 96ff24d2125a..82580ee5d483 100644 --- a/src/test/java/de/tum/in/www1/artemis/architecture/RepositoryArchitectureTest.java +++ b/src/test/java/de/tum/in/www1/artemis/architecture/RepositoryArchitectureTest.java @@ -127,12 +127,10 @@ void testTransactional() { // TODO: In the future we should reduce this number and eventually replace it by transactionalRule.check(allClasses) // The following methods currently violate this rule: // Method - // Method - // Method // Method // Method var result = transactionalRule.evaluate(allClasses); - Assertions.assertThat(result.getFailureReport().getDetails()).hasSize(5); + Assertions.assertThat(result.getFailureReport().getDetails()).hasSize(3); } @Test From 71171c05a96a70be6343c90c55196ed148a1cfb8 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 12 Jun 2024 16:10:17 +0200 Subject: [PATCH 52/73] avoid unnecessary DB call --- .../in/www1/artemis/service/exam/StudentExamService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java index f7958f498e14..c15266c4a31b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/StudentExamService.java @@ -692,8 +692,10 @@ public void setUpExerciseParticipationsAndSubmissions(StudentExam studentExam, L } } } - studentExam.setStudentParticipations(generatedParticipations); - this.studentExamRepository.save(studentExam); + if (!generatedParticipations.isEmpty()) { + studentExam.setStudentParticipations(generatedParticipations); + this.studentExamRepository.save(studentExam); + } } /** From 6c085c36c5492e5949a330dcbf72adc7798613dc Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 12 Jun 2024 16:30:40 +0200 Subject: [PATCH 53/73] adjust repository methods --- .../artemis/repository/StudentExamRepository.java | 12 +++++++++++- .../www1/artemis/web/rest/StudentExamResource.java | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index 0b01c781df0b..ab024e8c50d0 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -294,7 +294,17 @@ SELECT MAX(se.workingTime) AND se.exam.testExam = TRUE AND se.testRun = FALSE """) - List findStudentExamForTestExamsByUserIdAndCourseId(@Param("userId") Long userId, @Param("courseId") Long courseId); + List 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 findStudentExamsForTestExamsByUserIdAndExamId(@Param("userId") Long userId, @Param("examId") Long examId); @Query(""" SELECT DISTINCT se diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java index b5b4930bac22..00b18820983f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java @@ -463,7 +463,7 @@ public ResponseEntity> getStudentExamsForCoursePerUser(@PathVa User user = userRepository.getUserWithGroupsAndAuthorities(); studentExamAccessService.checkCourseAccessForStudentElseThrow(courseId, user); - List studentExamList = studentExamRepository.findStudentExamForTestExamsByUserIdAndCourseId(user.getId(), courseId); + List studentExamList = studentExamRepository.findStudentExamsForTestExamsByUserIdAndCourseId(user.getId(), courseId); return ResponseEntity.ok(studentExamList); } From 8c9f60442c36cb121c4b52234ceb994b515e0675 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 12 Jun 2024 16:30:59 +0200 Subject: [PATCH 54/73] rename method^ --- .../de/tum/in/www1/artemis/service/exam/ExamAccessService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java index 1ad2555ce57e..abd005a27fd1 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java @@ -79,7 +79,7 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { StudentExam studentExam; if (exam.isTestExam()) { - List unfinishedStudentExams = studentExamRepository.findStudentExamForTestExamsByUserIdAndCourseId(currentUser.getId(), courseId).stream() + List unfinishedStudentExams = studentExamRepository.findStudentExamsForTestExamsByUserIdAndExamId(currentUser.getId(), examId).stream() .filter(attempt -> !attempt.isFinished()).toList(); if (unfinishedStudentExams.isEmpty()) { studentExam = studentExamService.generateTestExam(exam, currentUser); From 6500a9ebfbfe940ec8d2cddace166540627b0e0d Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 12 Jun 2024 16:41:09 +0200 Subject: [PATCH 55/73] fix test --- .../de/tum/in/www1/artemis/service/exam/ExamServiceTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java index 7ff7552f5673..0bb2a29383a4 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java @@ -267,6 +267,7 @@ void testThrowsExceptionIfNotPublished() { @Test @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") void testDoesNotThrowExceptionForInstructors() { + studentExam.setId(1L); studentExam.setSubmitted(false); studentExam.getExam().setPublishResultsDate(ZonedDateTime.now().plusDays(5)); studentExam.getExam().setTestExam(true); // test runs are an edge case where instructors want to have access before the publishing date of results From 3b9a65615e1ad128df8b83e3681112191e3d3991 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 12 Jun 2024 17:06:40 +0200 Subject: [PATCH 56/73] remove unnecessary test --- .../service/ParticipationServiceTest.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java index 5eaf6bc70b8a..0ceded650304 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java @@ -148,23 +148,6 @@ void testGetBuildJobsForResultsOfParticipation() throws Exception { assertThat(programmingSubmission.getResults()).isNullOrEmpty(); } - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testStartExerciseWithInitializationDate_newParticipation() { - Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - Exercise modelling = course.getExercises().iterator().next(); - Participant participant = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); - ZonedDateTime initializationDate = ZonedDateTime.now().minusHours(5); - - StudentParticipation studentParticipationReceived = participationService.startExercise(modelling, participant, true); - - assertThat(studentParticipationReceived.getExercise()).isEqualTo(modelling); - assertThat(studentParticipationReceived.getStudent()).isPresent(); - assertThat(studentParticipationReceived.getStudent().get()).isEqualTo(participant); - assertThat(studentParticipationReceived.getInitializationDate()).isEqualTo(initializationDate); - assertThat(studentParticipationReceived.getInitializationState()).isEqualTo(InitializationState.INITIALIZED); - } - @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void canStartExerciseWithPracticeParticipationAfterDueDateChange() throws URISyntaxException { From 8fad45304aa7f5bcbb2a4044e382789bd9c4c906 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 14 Jun 2024 11:00:22 +0200 Subject: [PATCH 57/73] adjust translations --- src/main/webapp/i18n/de/exam.json | 2 +- src/main/webapp/i18n/en/exam.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index 075624ab9165..551796cc8097 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -293,7 +293,7 @@ "exercisePoints": "Punkte", "exerciseName": "Name", "noStudentExam": "Du bist nicht für die Klausur angemeldet. Bitte wende dich an die entsprechende Lehrkraft.", - "noFurtherAttempts": "Derzeit sind keine weiteren Versuche für die Testklausur möglich. Um deine Abgabe zu betrachten, navigiere zur Klausurseite ", + "noFurtherAttempts": "Derzeit sind keine weiteren Versuche für die Testklausur möglich. Um deine Abgaben zu betrachten, navigiere zur Klausurseite ", "atLeastTutorStudentExam": "Da du nicht Studierende:r bist, kannst du nicht an der Klausur teilnehmen:", "goToExamManagement": "Zur Klausurverwaltung gehen", "submitProgrammingExercise": "Weiter", diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index a2d3c5704f5e..b155b26c3a44 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -293,7 +293,7 @@ "exercisePoints": "Points", "exerciseName": "Name", "noStudentExam": "You are not registered for the exam. Please contact your instructor.", - "noFurtherAttempts": "Currently no further attempts for the Test Exam are possible. To review your submission, navigate back to the Exam page", + "noFurtherAttempts": "Currently no further attempts for the Test Exam are possible. To review your submissions, navigate back to the Exam page", "atLeastTutorStudentExam": "Since you are not a student, you cannot participate in this exam:", "goToExamManagement": "Go to exam management page", "submitProgrammingExercise": "Continue", From ac396fd084ae06b97b15dddf6d77000ab6a02bca Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Fri, 14 Jun 2024 12:05:07 +0200 Subject: [PATCH 58/73] refactor getExamInCourseElseThrow method --- .../service/exam/ExamAccessService.java | 89 +++++++++++-------- .../artemis/service/exam/ExamDateService.java | 10 +++ 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java index abd005a27fd1..8054e2db56c1 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java @@ -49,8 +49,11 @@ public class ExamAccessService { private final StudentExamService studentExamService; + private final ExamDateService examDateService; + public ExamAccessService(ExamRepository examRepository, StudentExamRepository studentExamRepository, AuthorizationCheckService authorizationCheckService, - UserRepository userRepository, CourseRepository courseRepository, ExamRegistrationService examRegistrationService, StudentExamService studentExamService) { + UserRepository userRepository, CourseRepository courseRepository, ExamRegistrationService examRegistrationService, StudentExamService studentExamService, + ExamDateService examDateService) { this.examRepository = examRepository; this.studentExamRepository = studentExamRepository; this.authorizationCheckService = authorizationCheckService; @@ -58,6 +61,7 @@ public ExamAccessService(ExamRepository examRepository, StudentExamRepository st this.courseRepository = courseRepository; this.examRegistrationService = examRegistrationService; this.studentExamService = studentExamService; + this.examDateService = examDateService; } /** @@ -76,42 +80,6 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, currentUser); Exam exam = examRepository.findWithExerciseGroupsAndExercisesByIdOrElseThrow(examId); - StudentExam studentExam; - - if (exam.isTestExam()) { - List unfinishedStudentExams = studentExamRepository.findStudentExamsForTestExamsByUserIdAndExamId(currentUser.getId(), examId).stream() - .filter(attempt -> !attempt.isFinished()).toList(); - if (unfinishedStudentExams.isEmpty()) { - studentExam = studentExamService.generateTestExam(exam, currentUser); - // For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource - studentExam.setExercises(null); - } - else if (unfinishedStudentExams.size() == 1) { - studentExam = unfinishedStudentExams.getFirst(); - } - else { - throw new IllegalStateException( - "User " + currentUser.getId() + " has " + unfinishedStudentExams.size() + " unfinished test exams for exam " + examId + " in course " + courseId); - } - // Check that the current user is registered for the test exam. Otherwise, the student can self-register - examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course, exam.getId(), currentUser); - - } - else { - // Check that the student exam exists - Optional optionalStudentExam = studentExamRepository.findByExamIdAndUserId(examId, currentUser.getId()); - // If an studentExam can be found, we can proceed - if (optionalStudentExam.isPresent()) { - studentExam = optionalStudentExam.get(); - } - else { - // We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam - // TODO Michal Kawka I think we can throw entity not found there - throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME, - "StudentExamGenerationOnlyForTestExams", true); - } - } - checkExamBelongsToCourseElseThrow(courseId, exam); if (!examId.equals(exam.getId())) { @@ -123,11 +91,58 @@ else if (unfinishedStudentExams.size() == 1) { throw new AccessForbiddenException(ENTITY_NAME, examId); } + if (exam.isTestExam()) { + return handleTestExam(exam, course, currentUser); + + } + else { + return handleExam(examId, currentUser.getId()); + } // NOTE: the check examRepository.isUserRegisteredForExam is not necessary because we already checked before that there is a student exam in this case for the current user + } + private StudentExam handleTestExam(Exam exam, Course course, User currentUser) { + StudentExam studentExam; + + if (this.examDateService.isExamOver(exam)) { + throw new BadRequestAlertException("Test exam has already ended", ENTITY_NAME, "examHasAlreadyEnded", true); + } + + List unfinishedStudentExams = studentExamRepository.findStudentExamsForTestExamsByUserIdAndExamId(currentUser.getId(), exam.getId()).stream() + .filter(attempt -> !attempt.isFinished()).toList(); + + if (unfinishedStudentExams.isEmpty()) { + studentExam = studentExamService.generateTestExam(exam, currentUser); + // For the start of the exam, the exercises are not needed. They are later loaded via StudentExamResource + studentExam.setExercises(null); + } + else if (unfinishedStudentExams.size() == 1) { + studentExam = unfinishedStudentExams.getFirst(); + } + else { + throw new IllegalStateException( + "User " + currentUser.getId() + " has " + unfinishedStudentExams.size() + " unfinished test exams for exam " + exam.getId() + " in course " + course.getId()); + } + // Check that the current user is registered for the test exam. Otherwise, the student can self-register + examRegistrationService.checkRegistrationOrRegisterStudentToTestExam(course, exam.getId(), currentUser); return studentExam; } + private StudentExam handleExam(Long examId, Long userId) { + // Check that the student exam exists + Optional optionalStudentExam = studentExamRepository.findByExamIdAndUserId(examId, userId); + // If an studentExam can be found, we can proceed + if (optionalStudentExam.isPresent()) { + return optionalStudentExam.get(); + } + else { + // We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam + // TODO Michal Kawka I think we can throw entity not found there + throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME, "StudentExamGenerationOnlyForTestExams", + true); + } + } + /** * Checks if the current user is allowed to manage exams of the given course. * diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java index dd0faafbc763..9f32ab983fc0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java @@ -36,6 +36,16 @@ public ExamDateService(ExamRepository examRepository, StudentExamRepository stud this.studentExamRepository = studentExamRepository; } + /** + * Returns if the exam is over by checking if the exam end date has passed. + * + * @param exam the exam + * @return true if the exam is over + */ + public boolean isExamOver(Exam exam) { + return exam.getEndDate().isBefore(ZonedDateTime.now()); + } + /** * Returns if the exam is over by checking if the latest individual exam end date plus grace period has passed. * See {@link ExamDateService#getLatestIndividualExamEndDate} From 0a2519ca340899634fdd3099eb88f6d15ece6cce Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 16 Jun 2024 01:13:16 +0200 Subject: [PATCH 59/73] translations --- .../course-exam-detail.component.html | 9 +++++++ .../course-exam-detail.component.ts | 24 +++++++++++++++++-- src/main/webapp/i18n/de/exam.json | 1 + src/main/webapp/i18n/en/exam.json | 1 + 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html b/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html index dce9951e1020..cc335cd8c668 100644 --- a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html +++ b/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html @@ -32,6 +32,10 @@

@case ('CONDUCTING') { } + + @case ('RESUME') { + + } @case ('TIMEEXTENSION') { @@ -75,6 +79,11 @@
{{ 'artemisApp.exam.overview.' + (exam.testExam ? 'testExam.' : '') + 'conducting' | artemisTranslate }}

} + @case ('RESUME') { +
+
{{ 'artemisApp.exam.overview.testExam.resume' | artemisTranslate }}
+
+ } @case ('TIMEEXTENSION') {
{{ 'artemisApp.exam.overview.timeExtension' | artemisTranslate }}
diff --git a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.ts b/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.ts index 4d2b2e922df8..21bc86bb3ec8 100644 --- a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.ts +++ b/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.ts @@ -26,6 +26,8 @@ export const enum ExamState { UNDEFINED = 'UNDEFINED', // Case 8: No more attempts NO_MORE_ATTEMPTS = 'NO_MORE_ATTEMPTS', + // Case 9: Resume attempt (only for test exams) + RESUME = 'RESUME', } @Component({ @@ -109,7 +111,7 @@ export class CourseExamDetailComponent implements OnInit, OnDestroy { this.timeLeftToStartInSeconds(); return; } - if (this.exam.endDate && dayjs().isBefore(this.exam.endDate)) { + if (!this.exam.testExam && this.exam.endDate && dayjs().isBefore(this.exam.endDate)) { this.examState = ExamState.CONDUCTING; return; } @@ -118,7 +120,7 @@ export class CourseExamDetailComponent implements OnInit, OnDestroy { } updateExamStateWithStudentExamOrTestExam() { - if (!this.studentExam && !this.exam.testExam && this.course?.id && this.exam?.id) { + if (!this.studentExam && this.course?.id && this.exam?.id) { this.examParticipationService .getOwnStudentExam(this.course.id, this.exam.id) .subscribe({ @@ -165,6 +167,14 @@ export class CourseExamDetailComponent implements OnInit, OnDestroy { this.cancelExamStateSubscription(); return; } + } else { + if (this.isWithinWorkingTime()) { + this.examState = ExamState.RESUME; + return; + } else if (this.exam.endDate && dayjs().isBefore(this.exam.endDate)) { + this.examState = ExamState.CONDUCTING; + return; + } } this.examState = ExamState.UNDEFINED; this.cancelExamStateSubscription(); @@ -176,4 +186,14 @@ export class CourseExamDetailComponent implements OnInit, OnDestroy { timeLeftToStartInSeconds() { this.timeLeftToStart = dayjs(this.exam.startDate!).diff(dayjs(), 'seconds'); } + + /** + * Determines if the given StudentExam is (still) within the working time + */ + isWithinWorkingTime() { + if (this.studentExam?.started && !this.studentExam.submitted && this.studentExam.startedDate && this.exam.workingTime) { + const endDate = dayjs(this.studentExam.startedDate).add(this.exam.workingTime, 'seconds'); + return dayjs(endDate).isAfter(dayjs()); + } + } } diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index 551796cc8097..87859627be73 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -57,6 +57,7 @@ "closed": "Testklausur beendet", "notSubmitted": "Du hast deine Testklausur nicht rechtzeitig eingereicht!", "reviewAttempt": "Versuch #{{attempt}}", + "resume": "Versuch fortführen", "resumeAttempt": "Versuch #{{attempt}} fortführen", "workingTimeLeft": "Verbleibende Arbeitszeit: ", "workingTimeCalculated": "Genutzte Arbeitszeit: ", diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index b155b26c3a44..63133d645d15 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -57,6 +57,7 @@ "closed": "Test Exam Closed", "notSubmitted": "You have not submitted your Test Exam on time!", "reviewAttempt": "Attempt #{{attempt}}", + "resume": "Resume Attempt", "resumeAttempt": "Resume Attempt #{{attempt}}", "workingTimeLeft": "Working time left: ", "workingTimeCalculated": "Used working time: ", From 6b4856e5c8ce02d5d059bd023c38e6d7ce4de972 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 16 Jun 2024 18:24:35 +0200 Subject: [PATCH 60/73] resolve rabbit comments --- .../www1/artemis/domain/exam/StudentExam.java | 6 ++++++ .../repository/StudentExamRepository.java | 21 +++++++++++++++++++ .../service/exam/ExamAccessService.java | 18 ++++++++++++++-- .../service/exam/ExamSubmissionService.java | 2 +- .../artemis/web/rest/StudentExamResource.java | 11 ++-------- .../quiz-participation.component.html | 4 ++-- ...se-exam-attempt-review-detail.component.ts | 6 +++--- .../course-exams/course-exams.component.html | 2 +- .../overview/course-overview.component.html | 4 ++-- 9 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java b/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java index 7a1c319e8fcf..a7217352503c 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java @@ -246,6 +246,12 @@ public Boolean isEnded() { return ZonedDateTime.now().isAfter(getIndividualEndDate()); } + /** + * Check if the individual student exam is finished + * A student exam is finished if it's started and either submitted or the time has passed + * + * @return true if the exam is finished, otherwise false + */ public boolean isFinished() { return Boolean.TRUE.equals(this.isStarted()) && (Boolean.TRUE.equals(this.isEnded()) || Boolean.TRUE.equals(this.isSubmitted())); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index ab024e8c50d0..182c8f849346 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -229,6 +229,27 @@ SELECT COUNT(se) """) Optional findLatestByExamIdAndUserId(@Param("examId") long examId, @Param("userId") long userId); + /** + * 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) { + Optional 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")); + } + /** * Checks if any StudentExam exists for the given user (student) id in the given course. * diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java index 8054e2db56c1..1311b7176f98 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamAccessService.java @@ -93,7 +93,6 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { if (exam.isTestExam()) { return handleTestExam(exam, course, currentUser); - } else { return handleExam(examId, currentUser.getId()); @@ -101,6 +100,15 @@ public StudentExam getExamInCourseElseThrow(Long courseId, Long examId) { // NOTE: the check examRepository.isUserRegisteredForExam is not necessary because we already checked before that there is a student exam in this case for the current user } + /** + * Fetches an unfinished StudentExam for a test exam if one exists. If no unfinished StudentExam exists, generates a new one. + * + * @param exam The exam which StudentExam belongs to + * @param course The course which the exam belongs to + * @return the StudentExam + * @throws BadRequestAlertException If the exam had already ended + * @throws IllegalStateException If the user has more than one unfinished student exam + */ private StudentExam handleTestExam(Exam exam, Course course, User currentUser) { StudentExam studentExam; @@ -128,6 +136,13 @@ else if (unfinishedStudentExams.size() == 1) { return studentExam; } + /** + * Fetches a real exam for the given examId and userId. + * + * @param examId the id of the Exam + * @param userId the id of the User + * @return the StudentExam + */ private StudentExam handleExam(Long examId, Long userId) { // Check that the student exam exists Optional optionalStudentExam = studentExamRepository.findByExamIdAndUserId(examId, userId); @@ -137,7 +152,6 @@ private StudentExam handleExam(Long examId, Long userId) { } else { // We skip the alert since this can happen when a tutor sees the exam card or the user did not participate yet is registered for the exam - // TODO Michal Kawka I think we can throw entity not found there throw new BadRequestAlertException("The requested Exam is no test exam and thus no student exam can be created", ENTITY_NAME, "StudentExamGenerationOnlyForTestExams", true); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java index e87eafe88a2e..ef06fafdbb08 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamSubmissionService.java @@ -152,7 +152,7 @@ private boolean isExamTestRunSubmission(Exercise exercise, User user, Exam exam) * @return the submission. If a submission already exists for the exercise we will set the id */ public Submission preventMultipleSubmissions(Exercise exercise, Submission submission, User user) { - // Return immediately if it is not an exam submissions or if it is a programming exercise or if it is a test exam exercise + // Return immediately if it is not an exam submission or if it is a programming exercise or if it is a test exam exercise if (!exercise.isExamExercise() || exercise instanceof ProgrammingExercise || exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { return submission; } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java index 00b18820983f..705ddb0ae293 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java @@ -563,15 +563,8 @@ public ResponseEntity> getExamLiveEvents(@PathVariabl log.debug("REST request to get the exam live events for exam {} by user {}", examId, currentUser.getLogin()); boolean testExam = examRepository.isTestExam(examId); - StudentExam studentExam; - if (testExam) { - studentExam = studentExamRepository.findLatestByExamIdAndUserId(examId, currentUser.getId()) - .orElseThrow(() -> new EntityNotFoundException("StudentExam for exam " + examId + " and user " + currentUser.getId() + " does not exist")); - } - else { - studentExam = studentExamRepository.findByExamIdAndUserId(examId, currentUser.getId()) - .orElseThrow(() -> new EntityNotFoundException("StudentExam for exam " + examId + " and user " + currentUser.getId() + " does not exist")); - } + + StudentExam studentExam = studentExamRepository.findOneByExamIdAndUserId(examId, currentUser.getId(), testExam); if (studentExam.isTestRun()) { throw new BadRequestAlertException("Test runs do not have live events", ENTITY_NAME, "testRunNoLiveEvents"); diff --git a/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html b/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html index 671e5afb13b2..d5a4948a9fbb 100644 --- a/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html +++ b/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html @@ -28,7 +28,7 @@

[translateValues]="{ userScore: userScore, maxScore: totalScore, - percentage: roundScoreSpecifiedByCourseSettings(result.score, getCourseFromExercise(quizExercise)), + percentage: roundScoreSpecifiedByCourseSettings(result.score, getCourseFromExercise(quizExercise)) }" >

} @@ -148,7 +148,7 @@

id="remaining-time-value" [ngClass]="{ 'time-critical': remainingTimeSeconds < 60 || remainingTimeSeconds < quizExercise.duration! / 4, - 'time-warning': remainingTimeSeconds < 120 || remainingTimeSeconds < quizExercise.duration! / 2, + 'time-warning': remainingTimeSeconds < 120 || remainingTimeSeconds < quizExercise.duration! / 2 }" > {{ remainingTimeText }} diff --git a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts index 50ee172cf117..a806813d7b29 100644 --- a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts +++ b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts @@ -86,7 +86,7 @@ export class CourseExamAttemptReviewDetailComponent implements OnInit, OnDestroy /** * do nothing if student didn't submit on time - * navigate to /courses/:courseId/exams/:examId/test-exam/:studentExamId if the attempt submitted + * navigate to /courses/:courseId/exams/:examId/test-exam/:studentExamId if the attempt was submitted * navigate to /courses/:courseId/exams/:examId/test-exam/start if the attempt can be continued * Used to open the corresponding studentExam */ @@ -96,11 +96,11 @@ export class CourseExamAttemptReviewDetailComponent implements OnInit, OnDestroy return; } // If exam is submitted navigate to the exam overview - else if (this.studentExam.submitted) { + if (this.studentExam.submitted) { this.router.navigate(['courses', this.courseId, 'exams', this.exam.id, 'test-exam', this.studentExam.id]); /// } // If exam is not submitted and within working time, resume attempt - else if (this.withinWorkingTime) { + if (this.withinWorkingTime) { this.router.navigate(['courses', this.courseId, 'exams', this.exam.id, 'test-exam', 'start']); } } diff --git a/src/main/webapp/app/overview/course-exams/course-exams.component.html b/src/main/webapp/app/overview/course-exams/course-exams.component.html index fc02eed0401f..c3f90f6cd349 100644 --- a/src/main/webapp/app/overview/course-exams/course-exams.component.html +++ b/src/main/webapp/app/overview/course-exams/course-exams.component.html @@ -28,7 +28,7 @@

Test Exams
diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index 4165710d0c9e..2ae30a6960cd 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -211,7 +211,7 @@ 'guided-tour': sidebarItem.guidedTour, newMessage: !messagesRouteLoaded && hasUnreadMessages && sidebarItem.title === 'Messages', collapsed: isNavbarCollapsed, - 'py-1': extraPadding, + 'py-1': extraPadding }" jhiOrionFilter [showInOrionWindow]="sidebarItem.showInOrionWindow" @@ -232,7 +232,7 @@ 'guided-tour': sidebarItem.guidedTour, newMessage: !messagesRouteLoaded && hasUnreadMessages && sidebarItem.title === 'Messages', collapsed: isNavbarCollapsed, - 'py-1': extraPadding, + 'py-1': extraPadding }" [routerLink]="sidebarItem.routerLink" routerLinkActive="active" From c6d851e8b674f1214f87fb6eafa68fb0f68e607f Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 16 Jun 2024 23:02:12 +0200 Subject: [PATCH 61/73] adjust tests --- .../participate/exam-participation.component.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts index f492a879713c..95e99f652f61 100644 --- a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts @@ -265,7 +265,7 @@ describe('ExamParticipationComponent', () => { expect(comp.exam).toEqual(studentExam.exam); }); - it('should load existing testExam if studentExam id is defined', () => { + it('should load existing testExam if studentExam id is start', () => { const studentExam = new StudentExam(); studentExam.exam = new Exam(); studentExam.exam.testExam = true; @@ -274,7 +274,7 @@ describe('ExamParticipationComponent', () => { studentExam.id = 4; const studentExamWithExercises = new StudentExam(); studentExamWithExercises.id = 4; - TestBed.inject(ActivatedRoute).params = of({ courseId: '1', examId: '2', studentExamId: '4' }); + TestBed.inject(ActivatedRoute).params = of({ courseId: '1', examId: '2', studentExamId: 'start' }); const loadStudentExamSpy = jest.spyOn(examParticipationService, 'getOwnStudentExam').mockReturnValue(of(studentExam)); const loadStudentExamWithExercisesForSummary = jest.spyOn(examParticipationService, 'loadStudentExamWithExercisesForSummary').mockReturnValue(of(studentExamWithExercises)); comp.ngOnInit(); @@ -301,7 +301,7 @@ describe('ExamParticipationComponent', () => { studentExam.ended = true; studentExam.submitted = true; comp.ngOnInit(); - expect(loadStudentExamSpy).toHaveBeenCalledOnce(); + expect(loadStudentExamSpy).not.toHaveBeenCalled(); expect(loadStudentExamWithExercisesForSummary).toHaveBeenCalledOnce(); expect(comp.studentExam).toEqual(studentExamWithExercises); expect(comp.studentExam).not.toEqual(studentExam); @@ -339,7 +339,7 @@ describe('ExamParticipationComponent', () => { }); const course: Course = { isAtLeastTutor: true }; - TestBed.inject(ActivatedRoute).params = of({ courseId: '1', examId: '2', studentExamId: '4' }); + TestBed.inject(ActivatedRoute).params = of({ courseId: '1', examId: '2' }); const loadStudentExamSpy = jest.spyOn(examParticipationService, 'getOwnStudentExam').mockReturnValue(throwError(() => httpError)); const courseStorageServiceSpy = jest.spyOn(courseStorageService, 'getCourse').mockReturnValue(course); comp.ngOnInit(); @@ -355,7 +355,7 @@ describe('ExamParticipationComponent', () => { }); const course: Course = { isAtLeastTutor: true }; - TestBed.inject(ActivatedRoute).params = of({ courseId: '1', examId: '2', studentExamId: '4' }); + TestBed.inject(ActivatedRoute).params = of({ courseId: '1', examId: '2' }); const loadStudentExamSpy = jest.spyOn(examParticipationService, 'getOwnStudentExam').mockReturnValue(throwError(() => httpError)); const courseStorageServiceSpy = jest.spyOn(courseStorageService, 'getCourse').mockReturnValue(undefined); const courseServiceSpy = jest.spyOn(courseService, 'find').mockReturnValue(of(new HttpResponse({ body: course }))); From b186bae27a0f05cf6fb77710857b49127f54c516 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 17 Jun 2024 12:23:57 +0200 Subject: [PATCH 62/73] adjust filtering of sensitive feedbacks in exam exercises --- .../www1/artemis/repository/StudentExamRepository.java | 9 +++++++++ .../de/tum/in/www1/artemis/service/ResultService.java | 5 ++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index 182c8f849346..2af31c76d69f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -229,6 +229,15 @@ SELECT COUNT(se) """) Optional 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 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. * diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java index b493518d7b9d..8fcf9dfe284d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java @@ -323,9 +323,8 @@ private void filterSensitiveFeedbackInCourseExercise(Participation participation private void filterSensitiveFeedbacksInExamExercise(Participation participation, Collection results, Exercise exercise) { Exam exam = exercise.getExerciseGroup().getExam(); boolean shouldResultsBePublished = exam.resultsPublished(); - if (!shouldResultsBePublished && exam.isTestExam() && participation instanceof StudentParticipation studentParticipation) { - var participant = studentParticipation.getParticipant(); - var studentExamOptional = studentExamRepository.findLatestByExamIdAndUserId(exam.getId(), participant.getId()); + if (!shouldResultsBePublished && exam.isTestExam() && participation instanceof StudentParticipation) { + var studentExamOptional = studentExamRepository.findByExamIdAndParticipationId(exam.getId(), participation.getId()); if (studentExamOptional.isPresent()) { shouldResultsBePublished = studentExamOptional.get().areResultsPublishedYet(); } From afeed60db9479269dea08c9172caf40fb623e0cd Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 17 Jun 2024 20:03:30 +0200 Subject: [PATCH 63/73] implement isTestExamExercise method --- src/main/java/de/tum/in/www1/artemis/domain/Exercise.java | 5 +++++ .../tum/in/www1/artemis/service/ParticipationService.java | 6 +++--- .../ProgrammingExerciseParticipationService.java | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java index d96a914ad3ec..6c5619abb78a 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java @@ -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 diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index 72884f7a1ad4..a88a5e9e35b8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -144,7 +144,7 @@ public StudentParticipation startExercise(Exercise exercise, Participant partici // 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.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { + if (exercise.isTestExamExercise()) { List participations = studentParticipationRepository.findByExerciseIdAndStudentId(exercise.getId(), participant.getId()); participations.forEach(studentParticipation -> studentParticipation.setInitializationState(InitializationState.FINISHED)); participation = createNewParticipation(exercise, participant); @@ -568,7 +568,7 @@ public Optional findOneByExerciseAndStudentLoginAnyState(E return optionalTeam.flatMap(team -> studentParticipationRepository.findOneByExerciseIdAndTeamId(exercise.getId(), team.getId())); } - if (exercise.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { + if (exercise.isTestExamExercise()) { return studentParticipationRepository.findLatestByExerciseIdAndStudentLogin(exercise.getId(), username); } @@ -647,7 +647,7 @@ public Optional findOneByExerciseAndStudentLoginWithEagerS return optionalTeam.flatMap(team -> studentParticipationRepository.findWithEagerLegalSubmissionsAndTeamStudentsByExerciseIdAndTeamId(exercise.getId(), team.getId())); } // If exercise is a test exam exercise we load the last participation, since there are multiple participations - if (exercise.isExamExercise() && exercise.getExamViaExerciseGroupOrCourseMember().isTestExam()) { + if (exercise.isTestExamExercise()) { return studentParticipationRepository.findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); } return studentParticipationRepository.findWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), username); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java index 4260bd8270b3..c3f0b233bcd4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java @@ -170,7 +170,7 @@ public ProgrammingExerciseStudentParticipation findStudentParticipationByExercis Optional participationOptional; - if (exercise.isExamExercise() && exercise.getExerciseGroup().getExam().isTestExam()) { + if (exercise.isTestExamExercise()) { if (withSubmissions) { participationOptional = studentParticipationRepository.findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun); } From 7b8ec724fe09f74299dd354150a8545558bd823e Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 17 Jun 2024 20:03:52 +0200 Subject: [PATCH 64/73] isTestExam method --- .../java/de/tum/in/www1/artemis/repository/ExamRepository.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java index b6ff3ccaa2c3..dd35bfb235bc 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ExamRepository.java @@ -515,10 +515,9 @@ private static Map convertListOfCountsIntoMap(List examId Set findActiveExams(@Param("courseIds") Set courseIds, @Param("userId") long userId, @Param("visible") ZonedDateTime visible, @Param("end") ZonedDateTime end); @Query(""" - SELECT COUNT(e) > 0 + SELECT e.testExam FROM Exam e WHERE e.id = :examId - AND e.testExam """) boolean isTestExam(@Param("examId") long examId); } From ee30325fb38b0f5673816f5b2e9979b48e3b5ebb Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 17 Jun 2024 20:30:23 +0200 Subject: [PATCH 65/73] add numberOfAttempts @param docu --- .../artemis/service/connectors/vcs/VersionControlService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java index 1714da10d72a..de1dd2e05120 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VersionControlService.java @@ -125,6 +125,7 @@ public interface VersionControlService { * @param sourceBranch The default branch of the source repository * @param targetProjectKey The key of the target project to which to copy the new repository to * @param targetRepositoryName The desired name of the target repository + * @param numberOfAttempts The attempt number * @return The URL for cloning the repository * @throws VersionControlException if the repository could not be copied on the VCS server (e.g. because the source repo does not exist) */ From c85c599e1f9333b106b46fe9925b3e4a24943930 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 24 Jun 2024 18:20:10 +0200 Subject: [PATCH 66/73] fix instructors not being able to participate --- ...xerciseStudentParticipationRepository.java | 15 ++++++++++ ...ogrammingExerciseParticipationService.java | 29 +++++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java index 7f909ed45e74..9d1e752718fd 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -75,18 +75,33 @@ Optional findByIdWithAllResultsAndRelat Optional findByExerciseIdAndStudentLogin(long exerciseId, String username); + Optional 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)); + } + @EntityGraph(type = LOAD, attributePaths = { "submissions" }) Optional findWithSubmissionsByExerciseIdAndStudentLogin(long exerciseId, String username); + @EntityGraph(type = LOAD, attributePaths = { "submissions" }) + Optional findFirstWithSubmissionsByExerciseIdAndStudentLoginOrderByIdDesc(long exerciseId, String username); + default ProgrammingExerciseStudentParticipation findWithSubmissionsByExerciseIdAndStudentLoginOrThrow(long exerciseId, String username) { return findWithSubmissionsByExerciseIdAndStudentLogin(exerciseId, username) .orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId)); } + default ProgrammingExerciseStudentParticipation findFirstWithSubmissionsByExerciseIdAndStudentLoginOrThrow(long exerciseId, String username) { + return findFirstWithSubmissionsByExerciseIdAndStudentLoginOrderByIdDesc(exerciseId, username) + .orElseThrow(() -> new EntityNotFoundException("Programming Exercise Student Participation", exerciseId)); + } + Optional findByExerciseIdAndStudentLoginAndTestRun(long exerciseId, String username, boolean testRun); @Query(""" diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java index c3f0b233bcd4..8be65d7080a2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java @@ -154,6 +154,29 @@ public ProgrammingExerciseStudentParticipation findTeamParticipationByExerciseAn return participationOptional.get(); } + @NotNull + public ProgrammingExerciseStudentParticipation findStudentParticipationByExerciseAndStudentLoginOrThrow(ProgrammingExercise exercise, String username, + boolean withSubmissions) { + + if (exercise.isTestExamExercise()) { + if (withSubmissions) { + return studentParticipationRepository.findFirstWithSubmissionsByExerciseIdAndStudentLoginOrThrow(exercise.getId(), username); + } + else { + return studentParticipationRepository.findFirstByExerciseIdAndStudentLoginOrThrow(exercise.getId(), username); + } + } + else { + if (withSubmissions) { + return studentParticipationRepository.findWithSubmissionsByExerciseIdAndStudentLoginOrThrow(exercise.getId(), username); + } + else { + return studentParticipationRepository.findByExerciseIdAndStudentLoginOrThrow(exercise.getId(), username); + } + + } + } + /** * Tries to retrieve a student participation for the given exercise and username and test run flag. * @@ -477,11 +500,7 @@ public ProgrammingExerciseParticipation getParticipationForRepository(Programmin boolean isExamEditorRepository = exercise.isExamExercise() && authorizationCheckService.isAtLeastEditorForExercise(exercise, userRepository.getUserByLoginElseThrow(repositoryTypeOrUserName)); if (isExamEditorRepository) { - if (withSubmissions) { - return studentParticipationRepository.findWithSubmissionsByExerciseIdAndStudentLoginOrThrow(exercise.getId(), repositoryTypeOrUserName); - } - - return studentParticipationRepository.findByExerciseIdAndStudentLoginOrThrow(exercise.getId(), repositoryTypeOrUserName); + return findStudentParticipationByExerciseAndStudentLoginOrThrow(exercise, repositoryTypeOrUserName, withSubmissions); } return findStudentParticipationByExerciseAndStudentLoginAndTestRunOrThrow(exercise, repositoryTypeOrUserName, isPracticeRepository, withSubmissions); From 7644eb601055179dcf52ec876152730f2252cec9 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 24 Jun 2024 18:21:34 +0200 Subject: [PATCH 67/73] prettier --- .../assessment-locks.component.html | 4 +- .../assessment-dashboard.component.html | 4 +- ...course-management-exercises.component.html | 2 +- ...sm-case-student-detail-view.component.html | 2 +- .../tutorial-groups-table.component.html | 2 +- .../exam-scores/exam-scores.component.html | 2 +- .../exam/manage/exam-status.component.html | 4 +- .../exam-checklist.component.html | 2 +- .../exercise-groups.component.html | 6 +-- .../students/exam-students.component.html | 4 +- .../exam-participation-cover.component.html | 2 +- .../exam-result-overview.component.html | 8 +-- .../modeling-submission.component.html | 2 +- .../git-diff-file-panel-title.component.html | 2 +- ...programming-exercise-detail.component.html | 2 +- .../programming-exercise.component.html | 6 +-- ...tory-and-build-plan-details.component.html | 12 ++--- ...drag-and-drop-question-edit.component.html | 2 +- .../quiz/manage/quiz-pool.component.html | 2 +- ...and-drop-question-statistic.component.html | 4 +- .../quiz-statistics-footer.component.html | 2 +- .../quiz-participation.component.html | 4 +- .../drag-and-drop-question.component.html | 10 ++-- ...-scoring-info-student-modal.component.html | 52 +++++++++---------- .../short-answer-question.component.html | 2 +- .../assessment-progress-label.component.html | 2 +- ...ercise-assessment-dashboard.component.html | 12 ++--- .../exam-exercise-row-buttons.component.html | 2 +- .../exercise-info.component.html | 6 +-- .../exercise-scores.component.html | 4 +- .../shared/feedback/feedback.component.html | 2 +- ...ded-in-overall-score-picker.component.html | 6 +-- .../plagiarism-sidebar.component.html | 2 +- .../team-participation-table.component.html | 2 +- .../build-agent-details.component.html | 10 ++-- .../build-queue/build-queue.component.html | 20 +++---- ...-exam-attempt-review-detail.component.html | 2 +- .../course-exam-detail.component.html | 4 +- .../overview/course-overview.component.html | 4 +- .../course-statistics.component.html | 4 +- ...ise-details-student-actions.component.html | 16 +++--- .../clone-repo-button.component.html | 2 +- .../sidebar-card-medium.component.html | 2 +- .../app/shared/sidebar/sidebar.component.html | 2 +- 44 files changed, 124 insertions(+), 124 deletions(-) diff --git a/src/main/webapp/app/assessment/assessment-locks/assessment-locks.component.html b/src/main/webapp/app/assessment/assessment-locks/assessment-locks.component.html index 6eb08480c1c6..3ddb1452b3b9 100644 --- a/src/main/webapp/app/assessment/assessment-locks/assessment-locks.component.html +++ b/src/main/webapp/app/assessment/assessment-locks/assessment-locks.component.html @@ -54,7 +54,7 @@

submission.participation!.id!, 'submissions', submission.id!, - 'assessment' + 'assessment', ]" class="btn btn-outline-secondary btn-sm mb-1" > @@ -70,7 +70,7 @@

submission.participation!.exercise!.id!, 'submissions', submission.id, - 'assessment' + 'assessment', ]" class="btn btn-outline-secondary btn-sm mb-1" > diff --git a/src/main/webapp/app/course/dashboards/assessment-dashboard/assessment-dashboard.component.html b/src/main/webapp/app/course/dashboards/assessment-dashboard/assessment-dashboard.component.html index 75dd01bd70be..6319c7dda35a 100644 --- a/src/main/webapp/app/course/dashboards/assessment-dashboard/assessment-dashboard.component.html +++ b/src/main/webapp/app/course/dashboards/assessment-dashboard/assessment-dashboard.component.html @@ -253,7 +253,7 @@

{{ 'artemisApp.assessmentDashboard.tutorPerformanceIssues.title' | artemisTr tutorName: issue.tutorName, numberOfTutorItems: issue.numberOfTutorItems, averageTutorValue: issue.averageTutorValue.toFixed(1), - threshold: issue.allowedRange.lowerBound.toFixed(1) + threshold: issue.allowedRange.lowerBound.toFixed(1), } }} @@ -267,7 +267,7 @@

{{ 'artemisApp.assessmentDashboard.tutorPerformanceIssues.title' | artemisTr tutorName: issue.tutorName, numberOfTutorItems: issue.numberOfTutorItems, averageTutorValue: issue.averageTutorValue.toFixed(1), - threshold: issue.allowedRange.upperBound.toFixed(1) + threshold: issue.allowedRange.upperBound.toFixed(1), } }} diff --git a/src/main/webapp/app/course/manage/course-management-exercises.component.html b/src/main/webapp/app/course/manage/course-management-exercises.component.html index 9122af2f1c1b..603b9c3c3c6e 100644 --- a/src/main/webapp/app/course/manage/course-management-exercises.component.html +++ b/src/main/webapp/app/course/manage/course-management-exercises.component.html @@ -44,7 +44,7 @@

course: course, programmingExerciseCountCallback: setProgrammingExerciseCount.bind(this), exerciseFilter: exerciseFilter, - filteredProgrammingExercisesCountCallback: setFilteredProgrammingExerciseCount.bind(this) + filteredProgrammingExercisesCountCallback: setFilteredProgrammingExerciseCount.bind(this), } " [embedded]="true" diff --git a/src/main/webapp/app/course/plagiarism-cases/student-view/detail-view/plagiarism-case-student-detail-view.component.html b/src/main/webapp/app/course/plagiarism-cases/student-view/detail-view/plagiarism-case-student-detail-view.component.html index 213513dca6ea..e33f0dfcc537 100644 --- a/src/main/webapp/app/course/plagiarism-cases/student-view/detail-view/plagiarism-case-student-detail-view.component.html +++ b/src/main/webapp/app/course/plagiarism-cases/student-view/detail-view/plagiarism-case-student-detail-view.component.html @@ -79,7 +79,7 @@

{{ 'artemisApp.plagiarism.plagiarismCases.conversation' | artemisTranslate } 'day' ) : posts[0].creationDate.add(7, 'day') - ).format('DD.MM.YYYY') + ).format('DD.MM.YYYY'), } " >

diff --git a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-groups-table/tutorial-groups-table.component.html b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-groups-table/tutorial-groups-table.component.html index ad622809e31a..08fc06f10250 100644 --- a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-groups-table/tutorial-groups-table.component.html +++ b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-groups-table/tutorial-groups-table.component.html @@ -73,7 +73,7 @@ [ngClass]="{ 'table-success': tutorialGroup.isUserRegistered, 'is-user-tutor': tutorialGroup.isUserTutor, - 'is-user-not-tutor': !tutorialGroup.isUserTutor + 'is-user-not-tutor': !tutorialGroup.isUserTutor, }" [tutorialGroup]="tutorialGroup" [course]="course" diff --git a/src/main/webapp/app/exam/exam-scores/exam-scores.component.html b/src/main/webapp/app/exam/exam-scores/exam-scores.component.html index 0806eac0f018..00e0d35e6eae 100644 --- a/src/main/webapp/app/exam/exam-scores/exam-scores.component.html +++ b/src/main/webapp/app/exam/exam-scores/exam-scores.component.html @@ -380,7 +380,7 @@

: { median: this.aggregatedExamResults.medianRelativePassed ? roundScoreSpecifiedByCourseSettings(this.aggregatedExamResults.medianRelativePassed, course) - : 0 + : 0, } }} diff --git a/src/main/webapp/app/exam/manage/exam-status.component.html b/src/main/webapp/app/exam/manage/exam-status.component.html index 245aa677f0c0..8e51dc8bc66b 100644 --- a/src/main/webapp/app/exam/manage/exam-status.component.html +++ b/src/main/webapp/app/exam/manage/exam-status.component.html @@ -62,7 +62,7 @@

{{ 'artemisApp.examStatus.preparation.' + (isTestExam ? | artemisTranslate : { generated: numberOfGeneratedStudentExams, - total: exam.numberOfExamUsers + total: exam.numberOfExamUsers, } }} @@ -240,7 +240,7 @@
{{ 'artemisApp.examStatus.correction.examCorrection' | a | artemisTranslate : { done: examChecklist.numberOfAllComplaintsDone!, - total: examChecklist.numberOfAllComplaints! + total: examChecklist.numberOfAllComplaints!, } }} diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html index 51902726da60..62378895923a 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html @@ -255,7 +255,7 @@

{{ 'artemisApp.examStatus.conduction.' + (isTestExam ? 'testExam.' : '') + ' | artemisTranslate : { from: exam.startDate! | artemisDate, - to: exam.endDate! | artemisDate + to: exam.endDate! | artemisDate, } }}
diff --git a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html index df0f821146f3..3ef9ff418472 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html +++ b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html @@ -143,7 +143,7 @@
{{ exerciseGroup.title }}
? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', - deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans' + deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans', } " (delete)="deleteExerciseGroup(exerciseGroup.id, $event)" @@ -254,7 +254,7 @@
{{ exerciseGroup.title }}
'exercise-groups', exerciseGroup.id, exercise.type + '-exercises', - exercise.id + exercise.id, ]" > {{ exercise.id }} @@ -278,7 +278,7 @@
{{ exerciseGroup.title }}
'exercise-groups', exerciseGroup.id, exercise.type + '-exercises', - exercise.id + exercise.id, ]" > {{ exercise.title }} diff --git a/src/main/webapp/app/exam/manage/students/exam-students.component.html b/src/main/webapp/app/exam/manage/students/exam-students.component.html index ebb65e1baa4e..471b3bcde32e 100644 --- a/src/main/webapp/app/exam/manage/students/exam-students.component.html +++ b/src/main/webapp/app/exam/manage/students/exam-students.component.html @@ -55,7 +55,7 @@

(delete)="removeAllStudents($event)" [dialogError]="dialogError$" [additionalChecks]="{ - deleteParticipationsAndSubmission: 'artemisApp.examManagement.examStudents.removeFromExam.deleteParticipationsAndSubmission' + deleteParticipationsAndSubmission: 'artemisApp.examManagement.examStudents.removeFromExam.deleteParticipationsAndSubmission', }" deleteConfirmationText="artemisApp.studentExams.removeAllStudents.confirmationText" > @@ -258,7 +258,7 @@

deleteConfirmationText="artemisApp.examManagement.examStudents.removeFromExam.typeNameToConfirm" (delete)="removeFromExam(value, $event)" [additionalChecks]="{ - deleteParticipationsAndSubmission: 'artemisApp.examManagement.examStudents.removeFromExam.deleteParticipationsAndSubmission' + deleteParticipationsAndSubmission: 'artemisApp.examManagement.examStudents.removeFromExam.deleteParticipationsAndSubmission', }" [dialogError]="dialogError$" [requireConfirmationOnlyForAdditionalChecks]="true" diff --git a/src/main/webapp/app/exam/participate/exam-cover/exam-participation-cover.component.html b/src/main/webapp/app/exam/participate/exam-cover/exam-participation-cover.component.html index 08d8251a3c6c..2380e389cd79 100644 --- a/src/main/webapp/app/exam/participate/exam-cover/exam-participation-cover.component.html +++ b/src/main/webapp/app/exam/participate/exam-cover/exam-participation-cover.component.html @@ -9,7 +9,7 @@

| artemisTranslate : { achievedPoints: overallAchievedPoints, - normalPoints: maxPoints + normalPoints: maxPoints, } }} } @@ -122,7 +122,7 @@

| artemisTranslate : { achievedPoints: overallAchievedPoints, - normalPoints: maxPoints + normalPoints: maxPoints, } }} } @@ -132,7 +132,7 @@

| artemisTranslate : { achievedBonus: studentExamWithGrade.studentResult.gradeWithBonus.bonusGrade, - bonusFromTitle: studentExamWithGrade.studentResult.gradeWithBonus.bonusFromTitle + bonusFromTitle: studentExamWithGrade.studentResult.gradeWithBonus.bonusFromTitle, } }} } diff --git a/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.html b/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.html index 118136ced22a..e15ee6e2a6fe 100644 --- a/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.html +++ b/src/main/webapp/app/exercises/modeling/participate/modeling-submission.component.html @@ -117,7 +117,7 @@

[ngClass]="{ 'text-secondary': feedback.isSubsequent, 'text-success': feedback.credits! > 0 && feedback.isSubsequent === undefined, - 'text-danger': feedback.credits! < 0 && feedback.isSubsequent === undefined + 'text-danger': feedback.credits! < 0 && feedback.isSubsequent === undefined, }" >Feedback: diff --git a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.html b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.html index a9328ed7f816..80802af1dbd1 100644 --- a/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.html +++ b/src/main/webapp/app/exercises/programming/hestia/git-diff-report/git-diff-file-panel-title.component.html @@ -7,7 +7,7 @@ badge: true, 'bg-success': fileStatus === FileStatus.CREATED, 'bg-warning': fileStatus === FileStatus.RENAMED, - 'bg-danger': fileStatus === FileStatus.DELETED + 'bg-danger': fileStatus === FileStatus.DELETED, }" > } diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html index 9adbffa0019d..b323c3a6f1a9 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html @@ -215,7 +215,7 @@

{{ ? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', - deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans' + deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans', } " deleteConfirmationText="artemisApp.exercise.delete.typeNameToConfirm" diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html index 2b133a7a219b..89d72f95b7ef 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html @@ -223,7 +223,7 @@ 'programming-exercises', programmingExercise.id, 'code-editor', - programmingExercise.templateParticipation.id + programmingExercise.templateParticipation.id, ]" class="btn btn-warning btn-sm me-1 mb-1" style="display: flex; justify-content: center; align-items: center" @@ -274,7 +274,7 @@ ? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', - deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans' + deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans', } " deleteConfirmationText="artemisApp.exercise.delete.typeNameToConfirm" @@ -324,7 +324,7 @@ ? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', - deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans' + deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans', } " class="me-1" diff --git a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.html b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.html index 53b0300ab054..8f4895072b94 100644 --- a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.html +++ b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.html @@ -11,7 +11,7 @@ repositoryBadge; context: { helpIconText: 'artemisApp.programmingExercise.preview.templateRepoTooltip', - trailingRepoSlug: '-exercise' + trailingRepoSlug: '-exercise', } " /> @@ -20,7 +20,7 @@ repositoryBadge; context: { helpIconText: 'artemisApp.programmingExercise.preview.solutionRepoTooltip', - trailingRepoSlug: '-solution' + trailingRepoSlug: '-solution', } " /> @@ -29,7 +29,7 @@ repositoryBadge; context: { helpIconText: 'artemisApp.programmingExercise.preview.testRepoTooltip', - trailingRepoSlug: '-tests' + trailingRepoSlug: '-tests', } " /> @@ -41,7 +41,7 @@ repositoryBadge; context: { trailingRepoSlug: '-' + auxiliaryRepository.name, - isAuxRepo: true + isAuxRepo: true, } " /> @@ -63,7 +63,7 @@ context: { helpIconText: 'artemisApp.programmingExercise.preview.templateBuildPlanTooltip', trailingRepoSlug: '-BASE', - isBuildPlan: true + isBuildPlan: true, } " /> @@ -73,7 +73,7 @@ context: { helpIconText: 'artemisApp.programmingExercise.preview.solutionBuildPlanTooltip', trailingRepoSlug: '-SOLUTION', - isBuildPlan: true + isBuildPlan: true, } " /> diff --git a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html index 14c98c85e745..183061aefb94 100644 --- a/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/drag-and-drop-question/drag-and-drop-question-edit.component.html @@ -297,7 +297,7 @@

[translateValues]="{ index: reason.translateValues.index, threshold: reason.translateValues.threshold, - name: reason.translateValues.name + name: reason.translateValues.name, }" >

} diff --git a/src/main/webapp/app/exercises/quiz/manage/statistics/drag-and-drop-question-statistic/drag-and-drop-question-statistic.component.html b/src/main/webapp/app/exercises/quiz/manage/statistics/drag-and-drop-question-statistic/drag-and-drop-question-statistic.component.html index 80bcf7aa8cb1..0bc5b17b77b1 100644 --- a/src/main/webapp/app/exercises/quiz/manage/statistics/drag-and-drop-question-statistic/drag-and-drop-question-statistic.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/statistics/drag-and-drop-question-statistic/drag-and-drop-question-statistic.component.html @@ -93,7 +93,7 @@

top: dropLocation.posY! / 2 + '%', left: dropLocation.posX! / 2 + '%', width: dropLocation.width! / 2 + '%', - height: dropLocation.height! / 2 + '%' + height: dropLocation.height! / 2 + '%', }" >
@@ -117,7 +117,7 @@

top: dropLocation.posY! / 2 + '%', left: dropLocation.posX! / 2 + '%', width: dropLocation.width! / 2 + '%', - height: dropLocation.height! / 2 + '%' + height: dropLocation.height! / 2 + '%', }" >
diff --git a/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistics-footer/quiz-statistics-footer.component.html b/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistics-footer/quiz-statistics-footer.component.html index e5aa1b70963f..5e8f3f7a77ac 100644 --- a/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistics-footer/quiz-statistics-footer.component.html +++ b/src/main/webapp/app/exercises/quiz/manage/statistics/quiz-statistics-footer/quiz-statistics-footer.component.html @@ -134,7 +134,7 @@ id="remaining-time-value" [ngClass]="{ 'time-critical': remainingTimeSeconds < 60 || remainingTimeSeconds < quizExercise.duration! / 4, - 'time-warning': remainingTimeSeconds < 120 || remainingTimeSeconds < quizExercise.duration! / 2 + 'time-warning': remainingTimeSeconds < 120 || remainingTimeSeconds < quizExercise.duration! / 2, }" > {{ remainingTimeText }} diff --git a/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html b/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html index 0c2b34063602..9751934f4645 100644 --- a/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html +++ b/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html @@ -28,7 +28,7 @@

[translateValues]="{ userScore: userScore, maxScore: totalScore, - percentage: roundScoreSpecifiedByCourseSettings(result.score, getCourseFromExercise(quizExercise)) + percentage: roundScoreSpecifiedByCourseSettings(result.score, getCourseFromExercise(quizExercise)), }" >

} @@ -148,7 +148,7 @@

id="remaining-time-value" [ngClass]="{ 'time-critical': remainingTimeSeconds < 60 || remainingTimeSeconds < quizExercise.duration! / 4, - 'time-warning': remainingTimeSeconds < 120 || remainingTimeSeconds < quizExercise.duration! / 2 + 'time-warning': remainingTimeSeconds < 120 || remainingTimeSeconds < quizExercise.duration! / 2, }" > {{ remainingTimeText }} diff --git a/src/main/webapp/app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component.html b/src/main/webapp/app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component.html index aae9a144d79a..45872ae42fa3 100644 --- a/src/main/webapp/app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component.html +++ b/src/main/webapp/app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component.html @@ -120,7 +120,7 @@

top: dropLocation.posY! / 2 + '%', left: dropLocation.posX! / 2 + '%', width: dropLocation.width! / 2 + '%', - height: dropLocation.height! / 2 + '%' + height: dropLocation.height! / 2 + '%', }" (cdkDropListDropped)="onDragDrop(dropLocation, $event)" (onDragEnter)="preventDefault($event)" @@ -154,13 +154,13 @@

isLocationCorrect(dropLocation) === MappingResult.MAPPED_INCORRECT && !dropLocation.invalid && !invalidDragItemForDropLocation(dropLocation) && - !question.invalid + !question.invalid, }" [ngStyle]="{ top: dropLocation.posY! / 2 + '%', left: dropLocation.posX! / 2 + '%', width: dropLocation.width! / 2 + '%', - height: dropLocation.height! / 2 + '%' + height: dropLocation.height! / 2 + '%', }" > @if ( @@ -220,13 +220,13 @@

isLocationCorrect(dropLocation) === MappingResult.MAPPED_INCORRECT && !dropLocation.invalid && !invalidDragItemForDropLocation(dropLocation) && - !question.invalid + !question.invalid, }" [ngStyle]="{ top: dropLocation.posY! / 2 + '%', left: dropLocation.posX! / 2 + '%', width: dropLocation.width! / 2 + '%', - height: dropLocation.height! / 2 + '%' + height: dropLocation.height! / 2 + '%', }" > @if ( diff --git a/src/main/webapp/app/exercises/quiz/shared/questions/quiz-scoring-infostudent-modal/quiz-scoring-info-student-modal.component.html b/src/main/webapp/app/exercises/quiz/shared/questions/quiz-scoring-infostudent-modal/quiz-scoring-info-student-modal.component.html index 0595cd2a3de8..de15c27dccb1 100644 --- a/src/main/webapp/app/exercises/quiz/shared/questions/quiz-scoring-infostudent-modal/quiz-scoring-info-student-modal.component.html +++ b/src/main/webapp/app/exercises/quiz/shared/questions/quiz-scoring-infostudent-modal/quiz-scoring-info-student-modal.component.html @@ -22,7 +22,7 @@

@@ -447,7 +447,7 @@

context: { submission: 'new', queryParams: getAssessmentQueryParams(correctionRound), - buttonLabel: 'startAssessment' + buttonLabel: 'startAssessment', } " id="start-new-assessment" @@ -494,7 +494,7 @@

buttonLabel: submission && calculateSubmissionStatusIsDraft(submission, correctionRound) ? 'continueAssessment' - : 'openAssessment' + : 'openAssessment', } " > @@ -551,7 +551,7 @@

jhiTranslate="artemisApp.exerciseAssessmentDashboard.noSubmissionsInfo" [translateValues]="{ notYetAssessed: notYetAssessed[correctionRound], - lockedSubmissionsByOtherTutor: lockedSubmissionsByOtherTutor[correctionRound] + lockedSubmissionsByOtherTutor: lockedSubmissionsByOtherTutor[correctionRound], }" > @if (correctionRound === 1) { @@ -653,7 +653,7 @@

submission: submissionWithComplaint.submission, queryParams: getComplaintQueryParams(submissionWithComplaint.complaint), disabled: isComplaintLocked(submissionWithComplaint.complaint), - buttonLabel: submissionWithComplaint.complaint.accepted === undefined ? 'evaluateComplaint' : 'showComplaint' + buttonLabel: submissionWithComplaint.complaint.accepted === undefined ? 'evaluateComplaint' : 'showComplaint', } " > @@ -777,7 +777,7 @@

buttonLabel: moreFeedbackRequest.complaint.accepted === undefined ? 'evaluateMoreFeedbackRequest' - : 'showMoreFeedbackRequests' + : 'showMoreFeedbackRequests', } " > diff --git a/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.html b/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.html index 864843d9ed93..b6b104d78205 100644 --- a/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.html +++ b/src/main/webapp/app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component.html @@ -153,7 +153,7 @@ ? {} : { deleteStudentReposBuildPlans: 'artemisApp.programmingExercise.delete.studentReposBuildPlans', - deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans' + deleteBaseReposBuildPlans: 'artemisApp.programmingExercise.delete.baseReposBuildPlans', } " > diff --git a/src/main/webapp/app/exercises/shared/exercise-info/exercise-info.component.html b/src/main/webapp/app/exercises/shared/exercise-info/exercise-info.component.html index cb0cd29f99d0..6730415e6a25 100644 --- a/src/main/webapp/app/exercises/shared/exercise-info/exercise-info.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-info/exercise-info.component.html @@ -19,7 +19,7 @@ [ngTemplateOutletContext]="{ key: 'artemisApp.courseOverview.exerciseDetails.submissionDue', value: dueDate ? dueDate : 'artemisApp.courseOverview.exerciseList.noDueDate', - isDate: !!dueDate + isDate: !!dueDate, }" /> @if (exercise.exampleSolutionPublicationDate) { @@ -28,7 +28,7 @@ [ngTemplateOutletContext]="{ key: 'artemisApp.courseOverview.exerciseDetails.exampleSolutionPublicationDate', value: exercise.exampleSolutionPublicationDate, - isDate: true + isDate: true, }" /> } @@ -50,7 +50,7 @@ [ngTemplateOutletContext]="{ key: 'artemisApp.courseOverview.exerciseDetails.complaintPossible', value: canComplainLaterOn ? 'global.generic.yes' : 'global.generic.no', - isDate: false + isDate: false, }" /> } diff --git a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.html b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.html index 864ffcf67aac..fcc5c6a7ea8d 100644 --- a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.html @@ -53,7 +53,7 @@

: { filtered: participationsPerFilter.get(resultCriteria.filterProp), total: filteredParticipations.length, - percent: ((participationsPerFilter.get(resultCriteria.filterProp) ?? 0) * 100) / filteredParticipations.length | number: '1.0-1' + percent: ((participationsPerFilter.get(resultCriteria.filterProp) ?? 0) * 100) / filteredParticipations.length | number: '1.0-1', } }} @@ -78,7 +78,7 @@

exercise.exerciseGroup!.id!, exercise.type + '-exercises', exercise.id, - 'participations' + 'participations', ]" class="btn btn-primary btn-sm me-1" > diff --git a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html index 8283bfd6acf5..f3808ab6a069 100644 --- a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html +++ b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html @@ -56,7 +56,7 @@

| artemisTranslate : { score: roundValueSpecifiedByCourseSettings(result.score ?? 0, course), - points: roundValueSpecifiedByCourseSettings(((result.score ?? 0) * exercise.maxPoints) / 100, course) + points: roundValueSpecifiedByCourseSettings(((result.score ?? 0) * exercise.maxPoints) / 100, course), } }} diff --git a/src/main/webapp/app/exercises/shared/included-in-overall-score-picker/included-in-overall-score-picker.component.html b/src/main/webapp/app/exercises/shared/included-in-overall-score-picker/included-in-overall-score-picker.component.html index cc4e056609f2..ca7687b905ed 100644 --- a/src/main/webapp/app/exercises/shared/included-in-overall-score-picker/included-in-overall-score-picker.component.html +++ b/src/main/webapp/app/exercises/shared/included-in-overall-score-picker/included-in-overall-score-picker.component.html @@ -3,7 +3,7 @@ class="btn" [ngClass]="{ 'btn-primary selected': includedInOverallScore === IncludedInOverallScore.INCLUDED_COMPLETELY, - 'btn-default': includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY + 'btn-default': includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY, }" (click)="change(IncludedInOverallScore.INCLUDED_COMPLETELY)" > @@ -13,7 +13,7 @@ class="btn btn-default" [ngClass]="{ 'btn-primary selected': includedInOverallScore === IncludedInOverallScore.INCLUDED_AS_BONUS, - 'btn-default': includedInOverallScore !== IncludedInOverallScore.INCLUDED_AS_BONUS + 'btn-default': includedInOverallScore !== IncludedInOverallScore.INCLUDED_AS_BONUS, }" (click)="change(IncludedInOverallScore.INCLUDED_AS_BONUS)" > @@ -24,7 +24,7 @@ class="btn btn-default" [ngClass]="{ 'btn-primary selected': includedInOverallScore === IncludedInOverallScore.NOT_INCLUDED, - 'btn-default': includedInOverallScore !== IncludedInOverallScore.NOT_INCLUDED + 'btn-default': includedInOverallScore !== IncludedInOverallScore.NOT_INCLUDED, }" (click)="change(IncludedInOverallScore.NOT_INCLUDED)" > diff --git a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-sidebar/plagiarism-sidebar.component.html b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-sidebar/plagiarism-sidebar.component.html index f8cab102f439..ef1a22f6f05b 100644 --- a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-sidebar/plagiarism-sidebar.component.html +++ b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-sidebar/plagiarism-sidebar.component.html @@ -25,7 +25,7 @@ class="plagiarism-status-indicator" [ngClass]="{ confirmed: comparison.status === CONFIRMED, - denied: comparison.status === DENIED + denied: comparison.status === DENIED, }" >
diff --git a/src/main/webapp/app/exercises/shared/team/team-participation-table/team-participation-table.component.html b/src/main/webapp/app/exercises/shared/team/team-participation-table/team-participation-table.component.html index 50fcf290c503..eaf550d3861b 100644 --- a/src/main/webapp/app/exercises/shared/team/team-participation-table/team-participation-table.component.html +++ b/src/main/webapp/app/exercises/shared/team/team-participation-table/team-participation-table.component.html @@ -85,7 +85,7 @@ value.id, 'participations', value.participation.id, - 'submissions' + 'submissions', ]" > {{ value.participation.id }} diff --git a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html index 3261d51b249d..ea1b2da0f1d2 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html +++ b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html @@ -111,7 +111,7 @@

@@ -130,7 +130,7 @@

{{ value }} @@ -150,7 +150,7 @@

{{ value }} @@ -238,7 +238,7 @@

{{ value }} @@ -254,7 +254,7 @@

{{ value }} diff --git a/src/main/webapp/app/localci/build-queue/build-queue.component.html b/src/main/webapp/app/localci/build-queue/build-queue.component.html index ccc88ebd1b84..658c33d8f61a 100644 --- a/src/main/webapp/app/localci/build-queue/build-queue.component.html +++ b/src/main/webapp/app/localci/build-queue/build-queue.component.html @@ -26,7 +26,7 @@

@if (row.jobTimingInfo.buildDuration > 240) { @@ -130,7 +130,7 @@

{{ value }} @@ -146,7 +146,7 @@

{{ value }} @@ -331,7 +331,7 @@

{{ value }} @@ -347,7 +347,7 @@

{{ value }} @@ -465,7 +465,7 @@

@@ -484,7 +484,7 @@

{{ value }} @@ -504,7 +504,7 @@

{{ value }} @@ -602,7 +602,7 @@

{{ value }} @@ -618,7 +618,7 @@

{{ value }} diff --git a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html index 56157ec9dc83..59d50ffd5913 100644 --- a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html +++ b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html @@ -5,7 +5,7 @@ [ngClass]="{ 'row card-body justify-content-center card-general-settings': true, 'bg-primary text-white': withinWorkingTime, - clickable: withinWorkingTime || studentExam.submitted + clickable: withinWorkingTime || studentExam.submitted, }" >
diff --git a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html b/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html index cc335cd8c668..99987ea0960e 100644 --- a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html +++ b/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html @@ -5,7 +5,7 @@ [ngClass]="{ 'row card-header': true, 'bg-primary': exam.testExam, - 'bg-success': !exam.testExam + 'bg-success': !exam.testExam, }" >
{{ exam.title }}
@@ -124,7 +124,7 @@
{{ 'artemisApp.exam.overview.testExam.noMoreAttempts' | | artemisTranslate : { startDate: exam.startDate | artemisDate, - endDate: exam.endDate | artemisDate + endDate: exam.endDate | artemisDate, } }}
diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index b86717da6ae4..dbf1a8edac4a 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -211,7 +211,7 @@ 'guided-tour': sidebarItem.guidedTour, newMessage: !messagesRouteLoaded && hasUnreadMessages && sidebarItem.title === 'Messages', collapsed: isNavbarCollapsed, - 'py-1': extraPadding + 'py-1': extraPadding, }" jhiOrionFilter [showInOrionWindow]="sidebarItem.showInOrionWindow" @@ -232,7 +232,7 @@ 'guided-tour': sidebarItem.guidedTour, newMessage: !messagesRouteLoaded && hasUnreadMessages && sidebarItem.title === 'Messages', collapsed: isNavbarCollapsed, - 'py-1': extraPadding + 'py-1': extraPadding, }" [routerLink]="sidebarItem.routerLink" routerLinkActive="active" diff --git a/src/main/webapp/app/overview/course-statistics/course-statistics.component.html b/src/main/webapp/app/overview/course-statistics/course-statistics.component.html index ad7405a39633..2bf923aec355 100644 --- a/src/main/webapp/app/overview/course-statistics/course-statistics.component.html +++ b/src/main/webapp/app/overview/course-statistics/course-statistics.component.html @@ -243,7 +243,7 @@

| artemisTranslate : { points: model.absoluteValue, - percentage: roundScoreSpecifiedByCourseSettings(model.value, course) + percentage: roundScoreSpecifiedByCourseSettings(model.value, course), } }} @@ -271,7 +271,7 @@

| artemisTranslate : { tests: model.absoluteValue, - percentage: roundScoreSpecifiedByCourseSettings(model.value, course) + percentage: roundScoreSpecifiedByCourseSettings(model.value, course), } }} diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html index 26d372f57d00..81177bce6787 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html @@ -18,8 +18,8 @@ icon: faPlayCircle, label: 'artemisApp.exerciseActions.openQuiz', quizMode: 'live', - hideLabelMobile: false - } + hideLabelMobile: false, + }, }" /> } @@ -32,8 +32,8 @@ icon: faPlayCircle, label: 'artemisApp.exerciseActions.startQuiz', quizMode: 'live', - hideLabelMobile: false - } + hideLabelMobile: false, + }, }" /> } @@ -45,8 +45,8 @@ icon: faEye, label: 'artemisApp.exerciseActions.viewSubmissions', quizMode: 'live', - outlined: true - } + outlined: true, + }, }" /> } @@ -58,8 +58,8 @@ icon: faEye, label: 'artemisApp.exerciseActions.viewResults', quizMode: 'live', - outlined: true - } + outlined: true, + }, }" /> } diff --git a/src/main/webapp/app/shared/components/clone-repo-button/clone-repo-button.component.html b/src/main/webapp/app/shared/components/clone-repo-button/clone-repo-button.component.html index 31d71240b54e..3d85457d90c9 100644 --- a/src/main/webapp/app/shared/components/clone-repo-button/clone-repo-button.component.html +++ b/src/main/webapp/app/shared/components/clone-repo-button/clone-repo-button.component.html @@ -44,7 +44,7 @@

{{ cloneHeadline | artemisTranslate }}
class="clone-url" [ngClass]="{ 'url-box-remove-line-left': sshEnabled && ((useSsh && !localVCEnabled) || !useSsh), - 'url-box-remove-line-right': !localVCEnabled && !(sshEnabled && ((useSsh && !localVCEnabled) || !useSsh)) + 'url-box-remove-line-right': !localVCEnabled && !(sshEnabled && ((useSsh && !localVCEnabled) || !useSsh)), }" [cdkCopyToClipboard]="getHttpOrSshRepositoryUri(false)" (cdkCopyToClipboardCopied)="onCopyFinished($event)" diff --git a/src/main/webapp/app/shared/sidebar/sidebar-card-medium/sidebar-card-medium.component.html b/src/main/webapp/app/shared/sidebar/sidebar-card-medium/sidebar-card-medium.component.html index b945b623dc5c..2ea00ba47527 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-card-medium/sidebar-card-medium.component.html +++ b/src/main/webapp/app/shared/sidebar/sidebar-card-medium/sidebar-card-medium.component.html @@ -5,7 +5,7 @@ 'border-success': sidebarItem?.difficulty === DifficultyLevel.EASY, 'border-warning': sidebarItem?.difficulty === DifficultyLevel.MEDIUM, 'border-danger': sidebarItem?.difficulty === DifficultyLevel.HARD, - 'border-module': !sidebarItem?.difficulty + 'border-module': !sidebarItem?.difficulty, }" [routerLink]="'./' + sidebarItem?.id" (click)="emitStoreAndRefresh(sidebarItem.id)" diff --git a/src/main/webapp/app/shared/sidebar/sidebar.component.html b/src/main/webapp/app/shared/sidebar/sidebar.component.html index 6a912026875d..b33a35e467ea 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar.component.html +++ b/src/main/webapp/app/shared/sidebar/sidebar.component.html @@ -18,7 +18,7 @@ [ngClass]="{ 'content-height-dev': !isProduction || isTestServer, 'search-height-conversations': sidebarData?.sidebarType === 'conversation', - 'search-height-normal': sidebarData?.sidebarType !== 'conversation' + 'search-height-normal': sidebarData?.sidebarType !== 'conversation', }" > @if (sidebarData?.groupByCategory && sidebarData.groupedData) { From 54853d5e0d1066fd730abb73e4bc9b971c9c83da Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 24 Jun 2024 23:43:51 +0200 Subject: [PATCH 68/73] remove LIMIT from JPQL queries --- .../domain/participation/Participation.java | 2 +- ...xerciseStudentParticipationRepository.java | 26 +++---------------- .../repository/StudentExamRepository.java | 13 ++-------- .../StudentParticipationRepository.java | 24 +++++++---------- .../artemis/service/ParticipationService.java | 22 ++++++---------- .../artemis/service/exam/ExamDateService.java | 2 +- ...ogrammingExerciseParticipationService.java | 5 ++-- 7 files changed, 28 insertions(+), 66 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java index 152dd4fb6cf8..6f27fa3328b3 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java @@ -119,7 +119,7 @@ public abstract class Participation extends DomainObject implements Participatio /** * 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 + * This field is necessary to preserve the constraint of one partipation per exercise, while allowing multiple particpiations per exercise for test exams * 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 diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java index 9d1e752718fd..386e098f1a93 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseStudentParticipationRepository.java @@ -104,17 +104,8 @@ default ProgrammingExerciseStudentParticipation findFirstWithSubmissionsByExerci Optional 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 - """) - Optional findLatestByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, @Param("username") String username, - @Param("testRun") boolean testRun); + Optional findFirstByExerciseIdAndStudentLoginAndTestRunOrderByIdDesc(@Param("exerciseId") long exerciseId, + @Param("username") String username, @Param("testRun") boolean testRun); @EntityGraph(type = LOAD, attributePaths = { "team.students" }) Optional findByExerciseIdAndTeamId(long exerciseId, long teamId); @@ -181,17 +172,8 @@ List findWithSubmissionsByExerciseIdAnd Optional 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 findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(@Param("exerciseId") long exerciseId, + @EntityGraph(type = LOAD, attributePaths = { "submissions" }) + Optional findFirstWithSubmissionsByExerciseIdAndStudentLoginAndTestRunOrderByIdDesc(@Param("exerciseId") long exerciseId, @Param("username") String username, @Param("testRun") boolean testRun); @Query(""" diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index 8bd4c8b17bc7..25a6c8a88cce 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -218,16 +218,7 @@ SELECT COUNT(se) """) Optional 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 findLatestByExamIdAndUserId(@Param("examId") long examId, @Param("userId") long userId); + Optional findFirstByExamIdAndUserIdOrderByIdDesc(@Param("examId") long examId, @Param("userId") long userId); @Query(""" SELECT se @@ -250,7 +241,7 @@ SELECT COUNT(se) default StudentExam findOneByExamIdAndUserId(long examId, long userId, boolean testExam) { Optional studentExam; if (testExam) { - studentExam = this.findLatestByExamIdAndUserId(examId, userId); + studentExam = this.findFirstByExamIdAndUserIdOrderByIdDesc(examId, userId); } else { studentExam = this.findByExamIdAndUserId(examId, userId); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index d98aa26e5836..923248243cfb 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -115,15 +115,7 @@ SELECT COUNT(p.id) > 0 """) Optional 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 findLatestByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username); + Optional findFirstByExerciseIdAndStudentLoginOrderByIdDesc(long exerciseId, String username); @Query(""" SELECT DISTINCT p @@ -139,11 +131,14 @@ SELECT COUNT(p.id) > 0 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 + WHERE p.id = ( + SELECT MAX(p2.id) + FROM StudentParticipation p2 + LEFT JOIN p2.submissions s2 + WHERE p2.exercise.id = :exerciseId + AND p2.student.login = :username + AND (s2.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR s2.type IS NULL) + ) """) Optional findLatestWithEagerLegalSubmissionsByExerciseIdAndStudentLogin(@Param("exerciseId") long exerciseId, @Param("username") String username); @@ -1019,7 +1014,6 @@ default List findByStudentExamWithEagerSubmissionsResult(S return findTestExamParticipationsByStudentIdAndIndividualExercisesWithEagerSubmissionsResultIgnoreTestRuns(studentExam.getId()); } } - else { if (withAssessor) { return findByStudentIdAndIndividualExercisesWithEagerSubmissionsResultAndAssessorIgnoreTestRuns(studentExam.getUser().getId(), studentExam.getExercises()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index a88a5e9e35b8..f7f6bb61dc6e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -177,7 +177,7 @@ public StudentParticipation startExercise(Exercise exercise, Participant partici if (exercise instanceof ProgrammingExercise programmingExercise) { // fetch again to get additional objects - participation = startProgrammingExercise(programmingExercise, (ProgrammingExerciseStudentParticipation) participation, false); + participation = startProgrammingExercise(programmingExercise, (ProgrammingExerciseStudentParticipation) participation); } // for all other exercises: QuizExercise, ModelingExercise, TextExercise, FileUploadExercise else { @@ -231,16 +231,15 @@ private StudentParticipation createNewParticipation(Exercise exercise, Participa * Start a programming exercise participation (which does not exist yet) by creating and configuring a student git repository (step 1) and a student build plan (step 2) * based on the templates in the given programming exercise * - * @param exercise the programming exercise that the currently active user (student) wants to start - * @param participation inactive participation - * @param setInitializationDate flag if the InitializationDate should be set to the current time + * @param exercise the programming exercise that the currently active user (student) wants to start + * @param participation inactive participation * @return started participation */ - private StudentParticipation startProgrammingExercise(ProgrammingExercise exercise, ProgrammingExerciseStudentParticipation participation, boolean setInitializationDate) { + private StudentParticipation startProgrammingExercise(ProgrammingExercise exercise, ProgrammingExerciseStudentParticipation participation) { // Step 1a) create the student repository (based on the template repository) participation = copyRepository(exercise, exercise.getVcsTemplateRepositoryUri(), participation); - return startProgrammingParticipation(exercise, participation, setInitializationDate); + return startProgrammingParticipation(exercise, participation); } /** @@ -267,10 +266,10 @@ private StudentParticipation startPracticeMode(ProgrammingExercise exercise, Pro // For practice mode 1 is always set. For more information see Participation.class participation.setNumberOfAttempts(1); - return startProgrammingParticipation(exercise, participation, true); + return startProgrammingParticipation(exercise, participation); } - private StudentParticipation startProgrammingParticipation(ProgrammingExercise exercise, ProgrammingExerciseStudentParticipation participation, boolean setInitializationDate) { + private StudentParticipation startProgrammingParticipation(ProgrammingExercise exercise, ProgrammingExerciseStudentParticipation participation) { // Step 1c) configure the student repository (e.g. access right, etc.) participation = configureRepository(exercise, participation); // Step 2a) create the build plan (based on the BASE build plan) @@ -281,11 +280,6 @@ private StudentParticipation startProgrammingParticipation(ProgrammingExercise e configureRepositoryWebHook(participation); // Step 4a) Set the InitializationState to initialized to indicate, the programming exercise is ready participation.setInitializationState(InitializationState.INITIALIZED); - // Step 4b) Set the InitializationDate to the current time - if (setInitializationDate) { - // Note: For test exams, the InitializationDate is set to the StudentExam: startedDate in {#link #startExerciseWithInitializationDate} - participation.setInitializationDate(ZonedDateTime.now()); - } // after saving, we need to make sure the object that is used after the if statement is the right one return participation; } @@ -569,7 +563,7 @@ public Optional findOneByExerciseAndStudentLoginAnyState(E } if (exercise.isTestExamExercise()) { - return studentParticipationRepository.findLatestByExerciseIdAndStudentLogin(exercise.getId(), username); + return studentParticipationRepository.findFirstByExerciseIdAndStudentLoginOrderByIdDesc(exercise.getId(), username); } return studentParticipationRepository.findByExerciseIdAndStudentLogin(exercise.getId(), username); diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java index 9f32ab983fc0..8d40849aba7c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDateService.java @@ -115,7 +115,7 @@ public boolean isIndividualExerciseWorkingPeriodOver(Exam exam, StudentParticipa // 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.findLatestByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId()); + optionalStudentExam = studentExamRepository.findFirstByExamIdAndUserIdOrderByIdDesc(exam.getId(), studentParticipation.getParticipant().getId()); } else { optionalStudentExam = studentExamRepository.findByExamIdAndUserId(exam.getId(), studentParticipation.getParticipant().getId()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java index 8be65d7080a2..23f78a932542 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java @@ -195,10 +195,11 @@ public ProgrammingExerciseStudentParticipation findStudentParticipationByExercis if (exercise.isTestExamExercise()) { if (withSubmissions) { - participationOptional = studentParticipationRepository.findLatestWithSubmissionsByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun); + participationOptional = studentParticipationRepository.findFirstWithSubmissionsByExerciseIdAndStudentLoginAndTestRunOrderByIdDesc(exercise.getId(), username, + isTestRun); } else { - participationOptional = studentParticipationRepository.findLatestByExerciseIdAndStudentLoginAndTestRun(exercise.getId(), username, isTestRun); + participationOptional = studentParticipationRepository.findFirstByExerciseIdAndStudentLoginAndTestRunOrderByIdDesc(exercise.getId(), username, isTestRun); } } else { From edc9c18543b263ba67d2268b292fe4de0b8e076e Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 24 Jun 2024 23:45:23 +0200 Subject: [PATCH 69/73] add ElseThrow to method signature^ --- .../tum/in/www1/artemis/repository/StudentExamRepository.java | 2 +- .../de/tum/in/www1/artemis/web/rest/StudentExamResource.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index 25a6c8a88cce..efe0f588fd7f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -238,7 +238,7 @@ SELECT COUNT(se) * @return the student exam * @throws EntityNotFoundException if no student exams could be found */ - default StudentExam findOneByExamIdAndUserId(long examId, long userId, boolean testExam) { + default StudentExam findOneByExamIdAndUserIdElseThrow(long examId, long userId, boolean testExam) { Optional studentExam; if (testExam) { studentExam = this.findFirstByExamIdAndUserIdOrderByIdDesc(examId, userId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java index 705ddb0ae293..9ed0e7c8c603 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java @@ -564,7 +564,7 @@ public ResponseEntity> getExamLiveEvents(@PathVariabl boolean testExam = examRepository.isTestExam(examId); - StudentExam studentExam = studentExamRepository.findOneByExamIdAndUserId(examId, currentUser.getId(), testExam); + StudentExam studentExam = studentExamRepository.findOneByExamIdAndUserIdElseThrow(examId, currentUser.getId(), testExam); if (studentExam.isTestRun()) { throw new BadRequestAlertException("Test runs do not have live events", ENTITY_NAME, "testRunNoLiveEvents"); From f913762d3164f2506f979c2eb1eb48e1f2e4f5e1 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 10 Jul 2024 18:40:35 +0200 Subject: [PATCH 70/73] Remove unused files in new exam mode ui --- ...-exam-attempt-review-detail.component.html | 64 ------ ...-exam-attempt-review-detail.component.scss | 23 -- ...se-exam-attempt-review-detail.component.ts | 107 ---------- .../course-exam-detail.component.html | 140 ------------ .../course-exam-detail.component.scss | 13 -- .../course-exam-detail.component.ts | 199 ------------------ 6 files changed, 546 deletions(-) delete mode 100644 src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html delete mode 100644 src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.scss delete mode 100644 src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts delete mode 100644 src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html delete mode 100644 src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.scss delete mode 100644 src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.ts diff --git a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html deleted file mode 100644 index 59d50ffd5913..000000000000 --- a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.html +++ /dev/null @@ -1,64 +0,0 @@ -@if (studentExam) { - -
-
- -

- @if (withinWorkingTime) { - - } - @if (!withinWorkingTime && studentExam.submitted) { - - } - @if (!withinWorkingTime && !studentExam.submitted) { - - } -

-
-
-
-
- {{ 'artemisApp.exam.overview.testExam.' + (withinWorkingTime ? 'resumeAttempt' : 'reviewAttempt') | artemisTranslate: { attempt: index } }} -
-
-
- @if (withinWorkingTime) { -
- {{ 'artemisApp.exam.overview.testExam.workingTimeLeft' | artemisTranslate }} {{ workingTimeLeftInSeconds() | artemisDurationFromSeconds: true }} -
- } - @if (studentExam.submitted) { -
- @if (studentExam.submissionDate) { -
- {{ 'artemisApp.exam.overview.testExam.submissionDate' | artemisTranslate }} {{ studentExam.submissionDate | artemisDate }} -
- } - @if (studentExam.submissionDate && studentExam.startedDate) { -
- {{ 'artemisApp.exam.overview.testExam.workingTimeCalculated' | artemisTranslate }} - -
- } -
- } - - @if (!withinWorkingTime && !studentExam.submitted) { -
-
{{ 'artemisApp.exam.overview.testExam.notSubmitted' | artemisTranslate }}
-
- } -
-
-
-
-
-} diff --git a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.scss b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.scss deleted file mode 100644 index 67a9d439025c..000000000000 --- a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.scss +++ /dev/null @@ -1,23 +0,0 @@ -.icon-settings { - display: flex; - justify-content: left; - align-items: center; - min-height: 50px; -} - -.card-general-settings { - padding: 5px 0; - border: 1px; - border-color: var(--primary); - border-radius: 3px; - transition: box-shadow 0.1s linear; - - .row { - min-height: 35px; - align-items: center; - } - - &:hover { - box-shadow: 0 2px 4px 0 var(--primary); - } -} diff --git a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts b/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts deleted file mode 100644 index a806813d7b29..000000000000 --- a/src/main/webapp/app/overview/course-exams/course-exam-attempt-review-detail/course-exam-attempt-review-detail.component.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { faCirclePlay, faFileCircleXmark, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; -import { StudentExam } from 'app/entities/student-exam.model'; -import dayjs from 'dayjs/esm'; -import { Exam } from 'app/entities/exam.model'; -import { Subscription, interval } from 'rxjs'; - -@Component({ - selector: 'jhi-course-exam-attempt-review-detail', - templateUrl: './course-exam-attempt-review-detail.component.html', - styleUrls: ['./course-exam-attempt-review-detail.component.scss'], -}) -export class CourseExamAttemptReviewDetailComponent implements OnInit, OnDestroy { - @Input() studentExam: StudentExam; - // Both needed for routing (and for the exam.workingTime) - @Input() exam: Exam; - @Input() courseId: number; - // Index used to enumerate the attempts per student - @Input() index: number; - @Input() latestExam: boolean; - studentExamState: Subscription; - - // Helper-Variables - withinWorkingTime: boolean; - - // Icons - faMagnifyingGlass = faMagnifyingGlass; - faCirclePlay = faCirclePlay; - faFileCircleXmark = faFileCircleXmark; - - constructor(private router: Router) {} - - /** - * Calculate the individual working time for every submitted StudentExam. As the StudentExam needs to be submitted, the - * working time cannot change. - * For the latest StudentExam, which is still within the allowed working time, a subscription is used to periodically check this. - */ - ngOnInit() { - if (this.studentExam.started && this.studentExam.submitted && this.studentExam.startedDate && this.studentExam.submissionDate) { - this.withinWorkingTime = false; - } else if (this.latestExam) { - // A subscription is used here to limit the number of calls for the countdown of the remaining workingTime. - this.studentExamState = interval(1000).subscribe(() => { - this.isWithinWorkingTime(); - // If the StudentExam is no longer within the working time, the subscription can be unsubscribed, as the state will not change anymore - if (!this.withinWorkingTime) { - this.unsubscribeFromExamStateSubscription(); - } - }); - } else { - this.withinWorkingTime = false; - } - } - - ngOnDestroy() { - this.unsubscribeFromExamStateSubscription(); - } - - /** - * Used to unsubscribe from the studentExamState Subscriptions - */ - unsubscribeFromExamStateSubscription() { - this.studentExamState?.unsubscribe(); - } - - /** - * Determines if the given StudentExam is (still) within the working time - */ - isWithinWorkingTime() { - if (this.studentExam.started && !this.studentExam.submitted && this.studentExam.startedDate && this.exam.workingTime) { - const endDate = dayjs(this.studentExam.startedDate).add(this.exam.workingTime, 'seconds'); - this.withinWorkingTime = dayjs(endDate).isAfter(dayjs()); - } - } - - /** - * Dynamically calculates the remaining working time of an attempt, if the attempt is started, within the working time and not yet submitted - */ - workingTimeLeftInSeconds(): number { - if (this.studentExam.started && !this.studentExam.submitted && this.studentExam.startedDate && this.exam.workingTime) { - return this.studentExam.startedDate.add(this.exam.workingTime, 'seconds').diff(dayjs(), 'seconds'); - } - return 0; - } - - /** - * do nothing if student didn't submit on time - * navigate to /courses/:courseId/exams/:examId/test-exam/:studentExamId if the attempt was submitted - * navigate to /courses/:courseId/exams/:examId/test-exam/start if the attempt can be continued - * Used to open the corresponding studentExam - */ - openStudentExam(): void { - // If student didn't submit on time, there's no exam overview - if (!this.withinWorkingTime && !this.studentExam.submitted) { - return; - } - // If exam is submitted navigate to the exam overview - if (this.studentExam.submitted) { - this.router.navigate(['courses', this.courseId, 'exams', this.exam.id, 'test-exam', this.studentExam.id]); /// - } - // If exam is not submitted and within working time, resume attempt - if (this.withinWorkingTime) { - this.router.navigate(['courses', this.courseId, 'exams', this.exam.id, 'test-exam', 'start']); - } - } -} diff --git a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html b/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html deleted file mode 100644 index 99987ea0960e..000000000000 --- a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.html +++ /dev/null @@ -1,140 +0,0 @@ -@if (exam) { -
- -
-
{{ exam.title }}
-
- -
- @if (examState === 'UNDEFINED') { -

- - -

- } @else { -
- @switch (examState) { - - @case ('UPCOMING') { - - } - - @case ('IMMINENT') { - - } - - @case ('CONDUCTING') { - - } - - @case ('RESUME') { - - } - - @case ('TIMEEXTENSION') { - - } - - @case ('STUDENTREVIEW') { - - } - - @case ('CLOSED') { - - } - - @case ('NO_MORE_ATTEMPTS') { - - } - } -
-
- @switch (examState) { - @case ('UPCOMING') { -
-
{{ 'artemisApp.exam.overview.' + (exam.testExam ? 'testExam.' : '') + 'upcoming' | artemisTranslate }}
-
- {{ 'artemisApp.exam.overview.' + (exam.testExam ? 'testExam.' : '') + 'imminent' | artemisTranslate }} - {{ timeLeftToStart | artemisDurationFromSeconds }} -
-
- } - @case ('IMMINENT') { -
-
- {{ 'artemisApp.exam.overview.' + (exam.testExam ? 'testExam.' : '') + 'imminent' | artemisTranslate }} - {{ timeLeftToStart | artemisDurationFromSeconds }} -
-
{{ 'artemisApp.exam.overview.' + (exam.testExam ? 'testExam.' : '') + 'imminentExplanation' | artemisTranslate }}
-
- } - @case ('CONDUCTING') { -
-
{{ 'artemisApp.exam.overview.' + (exam.testExam ? 'testExam.' : '') + 'conducting' | artemisTranslate }}
-
- } - @case ('RESUME') { -
-
{{ 'artemisApp.exam.overview.testExam.resume' | artemisTranslate }}
-
- } - @case ('TIMEEXTENSION') { -
-
{{ 'artemisApp.exam.overview.timeExtension' | artemisTranslate }}
-
{{ 'artemisApp.exam.overview.timeExtensionExplanation' | artemisTranslate }}
-
- } - @case ('CLOSED') { -
-
{{ 'artemisApp.exam.overview.' + (exam.testExam ? 'testExam.' : '') + 'closed' | artemisTranslate }}
-
- } - @case ('STUDENTREVIEW') { -
-
{{ 'artemisApp.exam.overview.review' | artemisTranslate }}
-
{{ 'artemisApp.exam.overview.reviewExplanation' | artemisTranslate }} {{ exam.examStudentReviewEnd | artemisDate }}
-
- } - @case ('NO_MORE_ATTEMPTS') { -
-
{{ 'artemisApp.exam.overview.testExam.noMoreAttempts' | artemisTranslate }}
-
{{ 'artemisApp.exam.overview.testExam.noMoreAttemptsExplanation' | artemisTranslate }}
-
- } - } -
- } -
- - -
-} diff --git a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.scss b/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.scss deleted file mode 100644 index 152d39297c48..000000000000 --- a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -.icon-settings { - display: flex; - justify-content: center; - align-items: center; - text-align: center; -} - -.card-general-settings { - padding: 5px 0; - border: 1px; - border-radius: 3px; - transition: box-shadow 0.1s linear; -} diff --git a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.ts b/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.ts deleted file mode 100644 index 21bc86bb3ec8..000000000000 --- a/src/main/webapp/app/overview/course-exams/course-exam-detail/course-exam-detail.component.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { Exam } from 'app/entities/exam.model'; -import { Course } from 'app/entities/course.model'; -import dayjs from 'dayjs/esm'; -import { faBook, faCalendarDay, faCirclePlay, faCircleStop, faMagnifyingGlass, faPenAlt, faPlay, faUserClock } from '@fortawesome/free-solid-svg-icons'; -import { Subscription, interval } from 'rxjs'; -import { StudentExam } from 'app/entities/student-exam.model'; -import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; - -// Enum to dynamically change the template-content -export const enum ExamState { - // Case 1: StartDate is at least 10min prior - UPCOMING = 'UPCOMING', - // Case 2: StartDate is [10min, 0min) prior - IMMINENT = 'IMMINENT', - // Case 3: Exam is open for participation - CONDUCTING = 'CONDUCTING', - // Case 4: Exam is closed, but student(s) with time extension may still write the exam. For these students, the exam is not yet closed. - TIMEEXTENSION = 'TIMEEXTENSION', - // Case 5: Exam is closed - CLOSED = 'CLOSED', - // Case 6: Exam is open for review - STUDENTREVIEW = 'STUDENTREVIEW', - // Case 7: Fallback - UNDEFINED = 'UNDEFINED', - // Case 8: No more attempts - NO_MORE_ATTEMPTS = 'NO_MORE_ATTEMPTS', - // Case 9: Resume attempt (only for test exams) - RESUME = 'RESUME', -} - -@Component({ - selector: 'jhi-course-exam-detail', - templateUrl: './course-exam-detail.component.html', - styleUrls: ['./course-exam-detail.component.scss'], -}) -export class CourseExamDetailComponent implements OnInit, OnDestroy { - @Input() exam: Exam; - @Input() course: Course; - // Interims-boolean to limit the number of attempts for a test exam (currently max 1 attempt) - @Input() maxAttemptsReached: boolean; - examState: ExamState; - examStateSubscription: Subscription; - timeLeftToStart: number; - - studentExam?: StudentExam; - - // Icons - faPenAlt = faPenAlt; - faCirclePlay = faCirclePlay; - faMagnifyingGlass = faMagnifyingGlass; - faCalendarDay = faCalendarDay; - faPlay = faPlay; - faUserClock = faUserClock; - faBook = faBook; - faCircleStop = faCircleStop; - - constructor( - private router: Router, - private examParticipationService: ExamParticipationService, - ) {} - - ngOnInit() { - // A subscription is used here to limit the number of calls - this.examStateSubscription = interval(1000).subscribe(() => { - this.updateExamState(); - }); - } - - ngOnDestroy() { - this.cancelExamStateSubscription(); - } - - cancelExamStateSubscription() { - this.examStateSubscription?.unsubscribe(); - } - - /** - * navigate to /courses/:courseId/exams/:examId for real exams or - * /courses/:courseId/exams/:examId/test-exam/start for test exams - */ - openExam() { - if (this.exam.testExam) { - if (this.examState === ExamState.NO_MORE_ATTEMPTS || this.examState === ExamState.CLOSED) { - return; - } - this.router.navigate(['courses', this.course.id, 'exams', this.exam.id, 'test-exam', 'start']); - } else { - this.router.navigate(['courses', this.course.id, 'exams', this.exam.id]); - } - // TODO: store the (plain) selected exam in the some service so that it can be obtained on other pages - // also make sure that the exam objects does not contain the course and all exercises - } - - /** - * Updates the status of the exam every second. The cases are explained at the ExamState enum. - */ - updateExamState() { - if (this.maxAttemptsReached) { - this.examState = ExamState.NO_MORE_ATTEMPTS; - this.cancelExamStateSubscription(); - return; - } - if (this.exam.startDate && dayjs().isBefore(this.exam.startDate)) { - if (dayjs(this.exam.startDate).diff(dayjs(), 'seconds') < 600) { - this.examState = ExamState.IMMINENT; - } else { - this.examState = ExamState.UPCOMING; - } - this.timeLeftToStartInSeconds(); - return; - } - if (!this.exam.testExam && this.exam.endDate && dayjs().isBefore(this.exam.endDate)) { - this.examState = ExamState.CONDUCTING; - return; - } - - this.updateExamStateWithStudentExamOrTestExam(); - } - - updateExamStateWithStudentExamOrTestExam() { - if (!this.studentExam && this.course?.id && this.exam?.id) { - this.examParticipationService - .getOwnStudentExam(this.course.id, this.exam.id) - .subscribe({ - next: (studentExam) => { - this.studentExam = studentExam; - }, - }) - .add(() => this.updateExamStateWithLoadedStudentExamOrTestExam()); - } else { - this.updateExamStateWithLoadedStudentExamOrTestExam(); - } - } - - updateExamStateWithLoadedStudentExamOrTestExam() { - const potentialLaterEndDate = this.studentExam?.workingTime ? dayjs(this.exam.startDate).add(this.studentExam.workingTime, 'seconds') : undefined; - const noOrOverPotentialLaterEndDate = !potentialLaterEndDate || dayjs().isAfter(potentialLaterEndDate); - if ((!this.studentExam || (!this.studentExam.submitted && noOrOverPotentialLaterEndDate)) && !this.exam.testExam) { - // Normal exam is over and student did not participate (no student exam was loaded nor submitted). We can cancel the subscription and the exam is closed - this.examState = ExamState.CLOSED; - this.cancelExamStateSubscription(); - return; - } - - if (this.exam.examStudentReviewStart && this.exam.examStudentReviewEnd && dayjs().isBetween(this.exam.examStudentReviewStart, this.exam.examStudentReviewEnd)) { - this.examState = ExamState.STUDENTREVIEW; - return; - } - if (dayjs().isAfter(this.exam.endDate)) { - if (!this.exam.testExam) { - // The longest individual working time is stored on the server side, but should not be extra loaded. Therefore, a sufficiently large time extension is selected. - const endDateWithTimeExtension = dayjs(this.exam.endDate).add(this.exam.workingTime! * 3, 'seconds'); - if (dayjs().isBefore(endDateWithTimeExtension)) { - this.examState = ExamState.TIMEEXTENSION; - return; - } else { - this.examState = ExamState.CLOSED; - return; - } - } else { - this.examState = ExamState.CLOSED; - // For test exams, we can cancel the subscription and lock the possibility to click on the exam tile - // For real exams, a CLOSED real exam can switch into a STUDENTREVIEW real exam - this.maxAttemptsReached = true; - this.cancelExamStateSubscription(); - return; - } - } else { - if (this.isWithinWorkingTime()) { - this.examState = ExamState.RESUME; - return; - } else if (this.exam.endDate && dayjs().isBefore(this.exam.endDate)) { - this.examState = ExamState.CONDUCTING; - return; - } - } - this.examState = ExamState.UNDEFINED; - this.cancelExamStateSubscription(); - } - - /** - * Dynamically calculates the time left until the exam start - */ - timeLeftToStartInSeconds() { - this.timeLeftToStart = dayjs(this.exam.startDate!).diff(dayjs(), 'seconds'); - } - - /** - * Determines if the given StudentExam is (still) within the working time - */ - isWithinWorkingTime() { - if (this.studentExam?.started && !this.studentExam.submitted && this.studentExam.startedDate && this.exam.workingTime) { - const endDate = dayjs(this.studentExam.startedDate).add(this.exam.workingTime, 'seconds'); - return dayjs(endDate).isAfter(dayjs()); - } - } -} From 2025853df5128d95119a05c0c6effb19c0561c91 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 10 Jul 2024 19:21:05 +0200 Subject: [PATCH 71/73] Implement new attempt design and integrate to new exam mode ui --- .../participate/exam-participation.route.ts | 11 -- .../course-exams/course-exams.component.ts | 115 ++++++++++++++++-- .../app/overview/course-overview.service.ts | 38 +++++- .../app/overview/courses-routing.module.ts | 13 ++ .../sidebar-card-item.component.html | 75 ++++++++---- src/main/webapp/app/types/sidebar.ts | 24 +++- .../webapp/i18n/de/student-dashboard.json | 5 + .../webapp/i18n/en/student-dashboard.json | 5 + 8 files changed, 241 insertions(+), 45 deletions(-) diff --git a/src/main/webapp/app/exam/participate/exam-participation.route.ts b/src/main/webapp/app/exam/participate/exam-participation.route.ts index a104b0ad964e..38ee3081db5e 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.route.ts +++ b/src/main/webapp/app/exam/participate/exam-participation.route.ts @@ -1,7 +1,6 @@ import { Routes } from '@angular/router'; import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; import { ExamParticipationComponent } from 'app/exam/participate/exam-participation.component'; -import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard'; import { Authority } from 'app/shared/constants/authority.constants'; import { GradingKeyOverviewComponent } from 'app/grading-system/grading-key-overview/grading-key-overview.component'; import { ExampleSolutionComponent } from 'app/exercises/shared/example-solution/example-solution.component'; @@ -35,16 +34,6 @@ export const examParticipationRoute: Routes = [ }, canActivate: [UserRouteAccessService], }, - { - path: 'test-exam/:studentExamId', - component: ExamParticipationComponent, - data: { - authorities: [Authority.USER], - pageTitle: 'artemisApp.exam.title', - }, - canActivate: [UserRouteAccessService], - canDeactivate: [PendingChangesGuard], - }, { path: 'exercises/:exerciseId/example-solution', component: ExampleSolutionComponent, diff --git a/src/main/webapp/app/overview/course-exams/course-exams.component.ts b/src/main/webapp/app/overview/course-exams/course-exams.component.ts index 0bccaf316ef3..7db930fc0b02 100644 --- a/src/main/webapp/app/overview/course-exams/course-exams.component.ts +++ b/src/main/webapp/app/overview/course-exams/course-exams.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Course } from 'app/entities/course.model'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { Subscription, interval } from 'rxjs'; import { Exam } from 'app/entities/exam.model'; import dayjs from 'dayjs/esm'; import { ArtemisServerDateService } from 'app/shared/server-date.service'; @@ -16,11 +16,13 @@ import { cloneDeep } from 'lodash-es'; const DEFAULT_UNIT_GROUPS: AccordionGroups = { real: { entityData: [] }, test: { entityData: [] }, + attempt: { entityData: [] }, }; const DEFAULT_COLLAPSE_STATE: CollapseState = { real: false, test: false, + attempt: false, }; @Component({ @@ -39,6 +41,7 @@ export class CourseExamsComponent implements OnInit, OnDestroy { public expandAttemptsMap = new Map(); public realExamsOfCourse: Exam[] = []; public testExamsOfCourse: Exam[] = []; + studentExamState: Subscription; // Icons faAngleUp = faAngleUp; @@ -47,12 +50,14 @@ export class CourseExamsComponent implements OnInit, OnDestroy { sortedRealExams?: Exam[]; sortedTestExams?: Exam[]; + testExamMap: Map = new Map(); examSelected = true; accordionExamGroups: AccordionGroups = DEFAULT_UNIT_GROUPS; sidebarData: SidebarData; sidebarExams: SidebarCardElement[] = []; isCollapsed = false; isExamStarted = false; + withinWorkingTime: boolean; readonly DEFAULT_COLLAPSE_STATE = DEFAULT_COLLAPSE_STATE; @@ -91,6 +96,7 @@ export class CourseExamsComponent implements OnInit, OnDestroy { .loadStudentExamsForTestExamsPerCourseAndPerUserForOverviewPage(this.courseId) .subscribe((response: StudentExam[]) => { this.studentExams = response!; + this.prepareSidebarData(); }); if (this.course?.exams) { @@ -111,7 +117,11 @@ export class CourseExamsComponent implements OnInit, OnDestroy { if (!examId && lastSelectedExam) { this.router.navigate([lastSelectedExam], { relativeTo: this.route, replaceUrl: true }); } else if (!examId && upcomingExam) { - this.router.navigate([upcomingExam.id], { relativeTo: this.route, replaceUrl: true }); + if (upcomingExam.testExam) { + this.router.navigate([upcomingExam.id + '/test-exam' + '/start'], { relativeTo: this.route, replaceUrl: true }); + } else { + this.router.navigate([upcomingExam.id], { relativeTo: this.route, replaceUrl: true }); + } } else { this.examSelected = examId ? true : false; } @@ -141,6 +151,7 @@ export class CourseExamsComponent implements OnInit, OnDestroy { } this.studentExamTestExamUpdateSubscription?.unsubscribe(); this.examStartedSubscription?.unsubscribe(); + this.unsubscribeFromExamStateSubscription(); } /** @@ -199,16 +210,27 @@ export class CourseExamsComponent implements OnInit, OnDestroy { const examCardItem = this.courseOverviewService.mapExamToSidebarCardElement(realExam); groupedExamGroups['real'].entityData.push(examCardItem); } - for (const testExam of testExams) { - const examCardItem = this.courseOverviewService.mapExamToSidebarCardElement(testExam); + testExams.forEach((testExam) => { + const examCardItem = this.courseOverviewService.mapExamToSidebarCardElement(testExam, this.getNumberOfAttemptsForTestExam(testExam)); groupedExamGroups['test'].entityData.push(examCardItem); - } - + const testExamAttempts = this.testExamMap.get(testExam.id!); + if (testExamAttempts) { + testExamAttempts.forEach((attempt, index) => { + const attemptNumber = testExamAttempts.length - index; + const attemptCardItem = this.courseOverviewService.mapAttemptToSidebarCardElement(attempt, attemptNumber); + groupedExamGroups['attempt'].entityData.push(attemptCardItem); + }); + } + }); return groupedExamGroups; } getLastSelectedExam(): string | null { - return sessionStorage.getItem('sidebar.lastSelectedItem.exam.byCourse.' + this.courseId); + let lastSelectedExam = sessionStorage.getItem('sidebar.lastSelectedItem.exam.byCourse.' + this.courseId); + if (lastSelectedExam && lastSelectedExam.startsWith('"') && lastSelectedExam.endsWith('"')) { + lastSelectedExam = lastSelectedExam.slice(1, -1); + } + return lastSelectedExam; } toggleSidebar() { @@ -233,11 +255,24 @@ export class CourseExamsComponent implements OnInit, OnDestroy { this.sortedRealExams = this.realExamsOfCourse.sort((a, b) => this.sortExamsByStartDate(a, b)); this.sortedTestExams = this.testExamsOfCourse.sort((a, b) => this.sortExamsByStartDate(a, b)); + for (const testExam of this.sortedTestExams) { + const orderedTestExamAttempts = this.getStudentExamForExamIdOrderedByIdReverse(testExam.id!); + orderedTestExamAttempts.forEach((attempt, index) => { + this.calculateIndividualWorkingTimeForTestExams(attempt, index === 0); + }); + const submittedAttempts = orderedTestExamAttempts.filter((attempt) => attempt.submitted); + this.testExamMap.set(testExam.id!, submittedAttempts); + } const sidebarRealExams = this.courseOverviewService.mapExamsToSidebarCardElements(this.sortedRealExams); const sidebarTestExams = this.courseOverviewService.mapExamsToSidebarCardElements(this.sortedTestExams); + const allStudentExams = this.getAllStudentExams(); + const sidebarTestExamAttempts = this.courseOverviewService.mapTestExamAttemptsToSidebarCardElements( + allStudentExams, + this.getIndicesForStudentExams(allStudentExams.length), + ); - this.sidebarExams = [...sidebarRealExams, ...sidebarTestExams]; + this.sidebarExams = [...sidebarRealExams, ...sidebarTestExams, ...(sidebarTestExamAttempts ?? [])]; this.accordionExamGroups = this.groupExamsByRealOrTest(this.sortedRealExams, this.sortedTestExams); this.updateSidebarData(); @@ -249,4 +284,68 @@ export class CourseExamsComponent implements OnInit, OnDestroy { } this.navigateToExam(); } + + // Method to iterate through the map and get all student exams + getAllStudentExams(): StudentExam[] { + const allStudentExams: StudentExam[] = []; + this.testExamMap.forEach((studentExams) => { + studentExams.forEach((studentExam) => { + allStudentExams.push(studentExam); + }); + }); + return allStudentExams; + } + + // Creating attempt indices for student exams + getIndicesForStudentExams(numberOfStudentExams: number): number[] { + const indices: number[] = []; + for (let i = 1; i <= numberOfStudentExams; i++) { + indices.push(i); + } + return indices; + } + + getNumberOfAttemptsForTestExam(exam: Exam): number { + const studentExams = this.testExamMap.get(exam.id!); + return studentExams ? studentExams.length : 0; + } + + /** + * Calculate the individual working time for every submitted StudentExam. As the StudentExam needs to be submitted, the + * working time cannot change. + * For the latest StudentExam, which is still within the allowed working time, a subscription is used to periodically check this. + */ + calculateIndividualWorkingTimeForTestExams(studentExam: StudentExam, latestExam: boolean) { + if (studentExam.started && studentExam.submitted && studentExam.startedDate && studentExam.submissionDate) { + this.withinWorkingTime = false; + } else if (latestExam) { + // A subscription is used here to limit the number of calls for the countdown of the remaining workingTime. + this.studentExamState = interval(1000).subscribe(() => { + this.isWithinWorkingTime(studentExam, studentExam.exam!); + // If the StudentExam is no longer within the working time, the subscription can be unsubscribed, as the state will not change anymore + if (!this.withinWorkingTime) { + this.unsubscribeFromExamStateSubscription(); + } + }); + } else { + this.withinWorkingTime = false; + } + } + + /** + * Used to unsubscribe from the studentExamState Subscriptions + */ + unsubscribeFromExamStateSubscription() { + this.studentExamState?.unsubscribe(); + } + + /** + * Determines if the given StudentExam is (still) within the working time + */ + isWithinWorkingTime(studentExam: StudentExam, exam: Exam) { + if (studentExam.started && !studentExam.submitted && studentExam.startedDate && exam.workingTime) { + const endDate = dayjs(studentExam.startedDate).add(exam.workingTime, 'seconds'); + this.withinWorkingTime = dayjs(endDate).isAfter(dayjs()); + } + } } diff --git a/src/main/webapp/app/overview/course-overview.service.ts b/src/main/webapp/app/overview/course-overview.service.ts index cfce72a8e649..b49423a97260 100644 --- a/src/main/webapp/app/overview/course-overview.service.ts +++ b/src/main/webapp/app/overview/course-overview.service.ts @@ -17,6 +17,7 @@ import { faBullhorn, faHashtag } from '@fortawesome/free-solid-svg-icons'; import { isOneToOneChatDTO } from 'app/entities/metis/conversation/one-to-one-chat.model'; import { isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model'; import { ConversationService } from 'app/shared/metis/conversations/conversation.service'; +import { StudentExam } from 'app/entities/student-exam.model'; const DEFAULT_UNIT_GROUPS: AccordionGroups = { future: { entityData: [] }, @@ -242,6 +243,12 @@ export class CourseOverviewService { return exams.map((exam) => this.mapExamToSidebarCardElement(exam)); } + mapTestExamAttemptsToSidebarCardElements(attempts?: StudentExam[], indices?: number[]) { + if (attempts && indices) { + return attempts.map((attempt, index) => this.mapAttemptToSidebarCardElement(attempt, index)); + } + } + mapConversationsToSidebarCardElements(conversations: ConversationDTO[]) { return conversations.map((conversation) => this.mapConversationToSidebarCardElement(conversation)); } @@ -292,16 +299,33 @@ export class CourseOverviewService { return exerciseCardItem; } - mapExamToSidebarCardElement(exam: Exam): SidebarCardElement { + mapExamToSidebarCardElement(exam: Exam, numberOfAttempts?: number): SidebarCardElement { const examCardItem: SidebarCardElement = { title: exam.title ?? '', - id: exam.id ?? '', + id: (exam.testExam ? exam.id + '/test-exam/' + 'start' : exam.id) ?? '', icon: faGraduationCap, subtitleLeft: exam.moduleNumber ?? '', startDateWithTime: exam.startDate, workingTime: exam.workingTime ?? 0, attainablePoints: exam.examMaxPoints ?? 0, size: 'L', + isAttempt: false, + testExam: exam.testExam, + attempts: numberOfAttempts ?? 0, + }; + return examCardItem; + } + + mapAttemptToSidebarCardElement(attempt: StudentExam, index: number): SidebarCardElement { + const examCardItem: SidebarCardElement = { + title: attempt.exam!.title ?? '', + id: attempt.exam!.id + '/test-exam/' + attempt.id, + icon: faGraduationCap, + subtitleLeft: this.translate.instant('artemisApp.courseOverview.sidebar.testExamAttempt') + ' ' + index, + submissionDate: attempt.submissionDate, + usedWorkingTime: this.calculateUsedWorkingTime(attempt), + size: 'L', + isAttempt: true, }; return examCardItem; } @@ -355,4 +379,14 @@ export class CourseOverviewService { setSidebarCollapseState(storageId: string, isCollapsed: boolean) { localStorage.setItem('sidebar.collapseState.' + storageId, JSON.stringify(isCollapsed)); } + + calculateUsedWorkingTime(studentExam: StudentExam): number { + let usedWorkingTime = 0; + if (studentExam.exam!.testExam && studentExam.started && studentExam.submitted && studentExam.workingTime && studentExam.startedDate && studentExam.submissionDate) { + const regularExamDuration = studentExam.workingTime; + // As students may submit during the grace period, the workingTime is limited to the regular exam duration + usedWorkingTime = Math.min(regularExamDuration, dayjs(studentExam.submissionDate).diff(dayjs(studentExam.startedDate), 'seconds')); + } + return usedWorkingTime; + } } diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 8b011de2206c..041335a426bc 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -245,6 +245,19 @@ const routes: Routes = [ canDeactivate: [PendingChangesGuard], loadChildren: () => import('../exam/participate/exam-participation.module').then((m) => m.ArtemisExamParticipationModule), }, + { + path: ':examId/test-exam/:studentExamId', + component: ExamParticipationComponent, + data: { + authorities: [Authority.USER], + pageTitle: 'overview.exams', + hasSidebar: true, + showRefreshButton: true, + }, + canActivate: [UserRouteAccessService], + canDeactivate: [PendingChangesGuard], + loadChildren: () => import('../exam/participate/exam-participation.module').then((m) => m.ArtemisExamParticipationModule), + }, ], }, { diff --git a/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.html b/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.html index d7171fafd9b3..ded76f134582 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.html +++ b/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.html @@ -11,38 +11,69 @@

-
+
{{ sidebarItem.subtitleLeft }}
- + - @if (sidebarItem.startDateWithTime) { - {{ sidebarItem.startDateWithTime | artemisDate: 'long-date' }} + @if (sidebarItem.isAttempt) { + {{ sidebarItem.submissionDate | artemisDate: 'long-date' }} - - {{ sidebarItem.startDateWithTime | artemisDate: 'time' }} - } - -
-
-
- - - @if (sidebarItem.workingTime) { - {{ sidebarItem.workingTime | artemisDurationFromSeconds }} + {{ sidebarItem.submissionDate | artemisDate: 'time' }} + } @else { + @if (sidebarItem.startDateWithTime) { + {{ sidebarItem.startDateWithTime | artemisDate: 'long-date' }} + - + {{ sidebarItem.startDateWithTime | artemisDate: 'time' }} + } }

-
- - - @if (sidebarItem.attainablePoints) { - {{ sidebarItem.attainablePoints }} - } - -
+ @if (sidebarItem.isAttempt) { +
+ + + @if (sidebarItem.usedWorkingTime) { + {{ sidebarItem.usedWorkingTime | artemisDurationFromSeconds }} + } @else { + {{ '-' }} + } + +
+ } @else { +
+ + + @if (sidebarItem.workingTime) { + {{ sidebarItem.workingTime | artemisDurationFromSeconds }} + } + +
+
+ @if (sidebarItem.testExam) { +
+ + + {{ sidebarItem.attempts }} + +
+ } @else { +
+ + + @if (sidebarItem.attainablePoints) { + {{ sidebarItem.attainablePoints }} + } + +
+ } + } } @else {
diff --git a/src/main/webapp/app/types/sidebar.ts b/src/main/webapp/app/types/sidebar.ts index 9cb59d946258..5cf3b8411985 100644 --- a/src/main/webapp/app/types/sidebar.ts +++ b/src/main/webapp/app/types/sidebar.ts @@ -6,7 +6,7 @@ import { ConversationDTO } from 'app/entities/metis/conversation/conversation.mo export type SidebarCardSize = 'S' | 'M' | 'L'; export type TimeGroupCategory = 'past' | 'current' | 'dueSoon' | 'future' | 'noDate'; -export type ExamGroupCategory = 'real' | 'test'; +export type ExamGroupCategory = 'real' | 'test' | 'attempt'; export type TutorialGroupCategory = 'all' | 'registered' | 'further'; export type SidebarTypes = 'exercise' | 'exam' | 'inExam' | 'conversation' | 'default'; export type AccordionGroups = Record; @@ -117,9 +117,29 @@ export interface SidebarCardElement { */ attainablePoints?: number; /** - * Set for Exam, indetifies the current status of an exam exercise for exam sidebar + * Set for Exam, identifies the current status of an exam exercise for exam sidebar */ rightIcon?: IconProp; + /* + * Set for Exam, identifies if it is a test exam attempt + */ + isAttempt?: boolean; + /* + * Set For Exam, identifies the number of attempts for each test exam + */ + attempts?: number; + /* + * Set For Exam, identifies if it is a test exam + */ + testExam?: boolean; + /* + * Set For Exam, identifies the submission date for an attempt of a test exam + */ + submissionDate?: dayjs.Dayjs; + /* + * Set For Exam, identifies used working time of a student for an attempt of a test exam + */ + usedWorkingTime?: number; /** * Set for Conversation. Will be removed after refactoring */ diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 38820f89e17b..f0a6d2b43a75 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -47,9 +47,14 @@ "further": "Weitere Übungsgruppen", "real": "Klausuren", "test": "Testklausuren", + "attempt": "Testklausurversuche", + "testExamAttempt": "Versuch", + "attempts": "Versuche", "start": "Beginn", "workingTime": "Bearbeitungszeit", "attainablePoints": "Erreichbare Punkte", + "submissionDate": "Abgabedatum", + "usedWorkingTime": "Gebrauchte Arbeitszeit", "noUpcomingSession": "Kein Termin", "header": "Konversationen", "show": "Konversationen zeigen", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 586cdf840ce1..1c77492419fc 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -47,9 +47,14 @@ "further": "Further Tutorial Groups", "real": "Exams", "test": "Test Exams", + "attempt": "Test Exam Attempts", + "testExamAttempt": "Attempt", + "attempts": "Attempts", "start": "Start", "workingTime": "Working Time", "attainablePoints": "Attainable Points", + "submissionDate": "Submission Date", + "usedWorkingTime": "Used Working Time", "noUpcomingSession": "No upcoming Session", "header": "Conversations", "show": "Show conversations", From 8870714575877d939ece52159506a0890aa1a134 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 10 Jul 2024 20:15:23 +0200 Subject: [PATCH 72/73] Fix one client test --- .../exam/participate/exam-participation.component.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts index 60cab6425b0e..ccc2642641f4 100644 --- a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts @@ -1065,7 +1065,6 @@ describe('ExamParticipationComponent', () => { const studentExam = new StudentExam(); studentExam.exam = new Exam(); - studentExam.exam.testExam = false; studentExam.exam.startDate = dayjs().subtract(2000, 'seconds'); studentExam.workingTime = 100; studentExam.id = 3; @@ -1074,8 +1073,9 @@ describe('ExamParticipationComponent', () => { TestBed.inject(ActivatedRoute).params = of({ courseId: '1', examId: '2', studentExamId: '3' }); jest.spyOn(examParticipationService, 'getOwnStudentExam').mockReturnValue(of(studentExam)); jest.spyOn(examParticipationService, 'loadStudentExamWithExercisesForSummary').mockReturnValue(of(studentExamWithExercises)); - comp.ngOnInit(); + + comp.testExam = false; comp.loadAndDisplaySummary(); expect(examLayoutStub).toHaveBeenCalledOnce(); @@ -1085,7 +1085,6 @@ describe('ExamParticipationComponent', () => { const examLayoutStub = jest.spyOn(examParticipationService, 'resetExamLayout'); const studentExam = new StudentExam(); studentExam.exam = new Exam(); - studentExam.exam.testExam = true; studentExam.exam.startDate = dayjs().subtract(2000, 'seconds'); studentExam.workingTime = 100; studentExam.id = 3; @@ -1094,8 +1093,9 @@ describe('ExamParticipationComponent', () => { TestBed.inject(ActivatedRoute).params = of({ courseId: '1', examId: '2', studentExamId: '3' }); jest.spyOn(examParticipationService, 'getOwnStudentExam').mockReturnValue(of(studentExam)); jest.spyOn(examParticipationService, 'loadStudentExamWithExercisesForSummary').mockReturnValue(of(studentExamWithExercises)); - comp.ngOnInit(); + + comp.testExam = true; comp.loadAndDisplaySummary(); expect(examLayoutStub).not.toHaveBeenCalledOnce(); From 1b6c16e176ab72c312573549840141115e4352b9 Mon Sep 17 00:00:00 2001 From: EgeDoguKaya Date: Wed, 10 Jul 2024 20:29:15 +0200 Subject: [PATCH 73/73] Add javadoc comments --- src/main/webapp/app/types/sidebar.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/types/sidebar.ts b/src/main/webapp/app/types/sidebar.ts index 5cf3b8411985..13b70665ef53 100644 --- a/src/main/webapp/app/types/sidebar.ts +++ b/src/main/webapp/app/types/sidebar.ts @@ -120,23 +120,23 @@ export interface SidebarCardElement { * Set for Exam, identifies the current status of an exam exercise for exam sidebar */ rightIcon?: IconProp; - /* + /** * Set for Exam, identifies if it is a test exam attempt */ isAttempt?: boolean; - /* + /** * Set For Exam, identifies the number of attempts for each test exam */ attempts?: number; - /* + /** * Set For Exam, identifies if it is a test exam */ testExam?: boolean; - /* + /** * Set For Exam, identifies the submission date for an attempt of a test exam */ submissionDate?: dayjs.Dayjs; - /* + /** * Set For Exam, identifies used working time of a student for an attempt of a test exam */ usedWorkingTime?: number;