From 446de0cf14afe56a69570e82b73e0c1b562acd63 Mon Sep 17 00:00:00 2001 From: Ole Vester Date: Sun, 4 Aug 2024 14:44:03 +0200 Subject: [PATCH 01/25] Add endpoint that generates exam summary (for deletion) --- .../repository/BuildJobRepository.java | 12 +++++ .../repository/StudentExamRepository.java | 2 + .../metis/AnswerPostRepository.java | 2 + .../repository/metis/PostRepository.java | 2 + .../service/exam/ExamDeletionService.java | 51 ++++++++++++++++++- .../www1/artemis/web/rest/ExamResource.java | 11 ++++ .../web/rest/dto/ExamDeletionSummaryDTO.java | 8 +++ 7 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/ExamDeletionSummaryDTO.java diff --git a/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java index dbce9f159046..dc94f8aee64e 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/BuildJobRepository.java @@ -103,4 +103,16 @@ Page findAllByFilterCriteria(@Param("buildStatus") BuildStatus buildStatus """) List getBuildJobsResultsStatistics(@Param("fromDateTime") ZonedDateTime fromDateTime, @Param("courseId") Long courseId); + /** + * Get the number of build jobs for exercise id. + */ + @Query(""" + SELECT COUNT(b) + FROM BuildJob b + LEFT JOIN Result r ON b.result.id = r.id + LEFT JOIN Participation p ON r.participation.id = p.id + LEFT JOIN Exercise e ON p.exercise.id = e.id + WHERE e.id = :exerciseId + """) + long countBuildJobsByExerciseId(@Param("exerciseId") Long exerciseId); } 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 864af9b0a336..9927f8f32509 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 @@ -270,6 +270,8 @@ SELECT MAX(se.workingTime) """) Set findAllUnsubmittedWithExercisesByExamId(@Param("examId") Long examId); + List findAllByExamId(Long examId); + List findAllByExamId_AndTestRunIsTrue(Long examId); @Query(""" diff --git a/src/main/java/de/tum/in/www1/artemis/repository/metis/AnswerPostRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/metis/AnswerPostRepository.java index 83fe9accfead..b586253093df 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/metis/AnswerPostRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/metis/AnswerPostRepository.java @@ -30,4 +30,6 @@ default AnswerPost findAnswerPostByIdElseThrow(Long answerPostId) { default AnswerPost findAnswerMessageByIdElseThrow(Long answerPostId) { return getValueElseThrow(findById(answerPostId).filter(answerPost -> answerPost.getPost().getConversation() != null), answerPostId); } + + long countAnswerPostsByPostIdIn(List postIds); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/metis/PostRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/metis/PostRepository.java index 60d6496bd049..899665395a41 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/metis/PostRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/metis/PostRepository.java @@ -61,4 +61,6 @@ default Post findPostByIdElseThrow(Long postId) throws EntityNotFoundException { default Post findPostOrMessagePostByIdElseThrow(Long postId) throws EntityNotFoundException { return getValueElseThrow(findById(postId), postId); } + + List findAllByConversationId(Long conversationId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDeletionService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDeletionService.java index 6311d2b0868d..e60c5e9e09fc 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDeletionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamDeletionService.java @@ -22,11 +22,14 @@ import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.GradingScale; import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.domain.enumeration.ExerciseType; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; import de.tum.in.www1.artemis.domain.exam.StudentExam; +import de.tum.in.www1.artemis.domain.metis.Post; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; import de.tum.in.www1.artemis.domain.quiz.QuizPool; +import de.tum.in.www1.artemis.repository.BuildJobRepository; import de.tum.in.www1.artemis.repository.ExamLiveEventRepository; import de.tum.in.www1.artemis.repository.ExamRepository; import de.tum.in.www1.artemis.repository.GradingScaleRepository; @@ -34,10 +37,13 @@ import de.tum.in.www1.artemis.repository.StudentExamRepository; import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.repository.metis.AnswerPostRepository; +import de.tum.in.www1.artemis.repository.metis.PostRepository; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; import de.tum.in.www1.artemis.service.ExerciseDeletionService; import de.tum.in.www1.artemis.service.ParticipationService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; +import de.tum.in.www1.artemis.web.rest.dto.ExamDeletionSummaryDTO; @Profile(PROFILE_CORE) @Service @@ -71,10 +77,17 @@ public class ExamDeletionService { private final QuizPoolRepository quizPoolRepository; + private final BuildJobRepository buildJobRepository; + + private final PostRepository postRepository; + + private final AnswerPostRepository answerPostRepository; + public ExamDeletionService(ExerciseDeletionService exerciseDeletionService, ParticipationService participationService, CacheManager cacheManager, UserRepository userRepository, ExamRepository examRepository, AuditEventRepository auditEventRepository, StudentExamRepository studentExamRepository, GradingScaleRepository gradingScaleRepository, StudentParticipationRepository studentParticipationRepository, ChannelRepository channelRepository, ChannelService channelService, - ExamLiveEventRepository examLiveEventRepository, QuizPoolRepository quizPoolRepository) { + ExamLiveEventRepository examLiveEventRepository, QuizPoolRepository quizPoolRepository, BuildJobRepository buildJobRepository, PostRepository postRepository, + AnswerPostRepository answerPostRepository) { this.exerciseDeletionService = exerciseDeletionService; this.participationService = participationService; this.cacheManager = cacheManager; @@ -88,6 +101,9 @@ public ExamDeletionService(ExerciseDeletionService exerciseDeletionService, Part this.channelService = channelService; this.examLiveEventRepository = examLiveEventRepository; this.quizPoolRepository = quizPoolRepository; + this.buildJobRepository = buildJobRepository; + this.postRepository = postRepository; + this.answerPostRepository = answerPostRepository; } /** @@ -240,4 +256,37 @@ public void deleteTestRun(Long testRunId) { log.info("Request to delete Test Run {}", testRunId); studentExamRepository.deleteById(testRunId); } + + /** + * Get the exam deletion summary for the given exam. + * + * @param examId the ID of the exam for which the deletion summary should be fetched + * @return the exam deletion summary + */ + public ExamDeletionSummaryDTO getExamDeletionSummary(@NotNull long examId) { + Exam exam = examRepository.findOneWithEagerExercisesGroupsAndStudentExams(examId); + long numberOfBuilds = 0; + for (ExerciseGroup exerciseGroup : exam.getExerciseGroups()) { + var programmingExercises = exerciseGroup.getExercises().stream().filter(exercise -> ExerciseType.PROGRAMMING.equals(exercise.getExerciseType())).toList(); + + for (Exercise exercise : programmingExercises) { + numberOfBuilds += buildJobRepository.countBuildJobsByExerciseId(exercise.getId()); + } + } + Channel channel = channelRepository.findChannelByExamId(examId); + Long conversationId = channel.getId(); + + List posts = postRepository.findAllByConversationId(conversationId); + long numberOfCommunicationPosts = posts.size(); + long numberOfAnswerPosts = answerPostRepository.countAnswerPostsByPostIdIn(posts.stream().map(Post::getId).toList()); + + List studentExams = studentExamRepository.findAllByExamId(examId); + long numberRegisteredStudents = studentExams.size(); + + long notStartedExams = studentExams.stream().filter(studentExam -> !studentExam.isStarted()).count(); + long startedExams = studentExams.stream().filter(studentExam -> studentExam.isStarted() && !studentExam.isSubmitted()).count(); + long submittedExams = studentExams.stream().filter(StudentExam::isSubmitted).count(); + + return new ExamDeletionSummaryDTO(numberOfBuilds, numberOfCommunicationPosts, numberOfAnswerPosts, numberRegisteredStudents, notStartedExams, startedExams, submittedExams); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java index bc3cbf5f8dff..1a69fcb4e4e4 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java @@ -95,6 +95,7 @@ import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.CourseWithIdDTO; import de.tum.in.www1.artemis.web.rest.dto.ExamChecklistDTO; +import de.tum.in.www1.artemis.web.rest.dto.ExamDeletionSummaryDTO; import de.tum.in.www1.artemis.web.rest.dto.ExamInformationDTO; import de.tum.in.www1.artemis.web.rest.dto.ExamScoresDTO; import de.tum.in.www1.artemis.web.rest.dto.ExamUserDTO; @@ -1321,4 +1322,14 @@ public ResponseEntity> getAllSuspiciousExamSessio analyzeSessionsIpOutsideOfRange); return ResponseEntity.ok(examSessionService.retrieveAllSuspiciousExamSessionsByExamId(examId, options, Optional.ofNullable(ipSubnet))); } + + @GetMapping("courses/{courseId}/exams/{examId}/deletion-summary") + @EnforceAtLeastInstructor + public ResponseEntity getDeletionSummary(@PathVariable long courseId, @PathVariable long examId) { + log.debug("REST request to get deletion summary for exam : {}", examId); + Course course = courseRepository.findByIdElseThrow(courseId); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); + + return ResponseEntity.ok(examDeletionService.getExamDeletionSummary(examId)); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ExamDeletionSummaryDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ExamDeletionSummaryDTO.java new file mode 100644 index 000000000000..d48babd11d16 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ExamDeletionSummaryDTO.java @@ -0,0 +1,8 @@ +package de.tum.in.www1.artemis.web.rest.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record ExamDeletionSummaryDTO(long numberOfBuilds, long numberOfCommunicationPosts, long numberOfAnswerPosts, long numberRegisteredStudents, long numberNotStartedExams, + long numberStartedExams, long numberSubmittedExams) { +} From 68f3eab299320d4de225e981b1346874f5d7c88c Mon Sep 17 00:00:00 2001 From: Ole Vester Date: Sun, 4 Aug 2024 14:57:07 +0200 Subject: [PATCH 02/25] Extend delete popup with summary of entity to be deleted --- .../delete-dialog/delete-button.directive.ts | 4 ++++ .../delete-dialog/delete-dialog.component.html | 15 +++++++++++++++ .../delete-dialog/delete-dialog.component.ts | 2 ++ .../shared/delete-dialog/delete-dialog.model.ts | 6 ++++++ .../shared/delete-dialog/delete-dialog.service.ts | 2 ++ 5 files changed, 29 insertions(+) diff --git a/src/main/webapp/app/shared/delete-dialog/delete-button.directive.ts b/src/main/webapp/app/shared/delete-dialog/delete-button.directive.ts index 4f1476ae508b..fd2d89600490 100644 --- a/src/main/webapp/app/shared/delete-dialog/delete-button.directive.ts +++ b/src/main/webapp/app/shared/delete-dialog/delete-button.directive.ts @@ -9,6 +9,8 @@ import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; export class DeleteButtonDirective implements OnInit { @Input() entityTitle?: string; @Input() deleteQuestion: string; + @Input() entitySummaryTitle: string; + @Input() entitySummary: { [key: string]: unknown } = {}; @Input() translateValues: { [key: string]: unknown } = {}; @Input() deleteConfirmationText: string; @Input() buttonSize: ButtonSize = ButtonSize.SMALL; @@ -73,6 +75,8 @@ export class DeleteButtonDirective implements OnInit { translateValues: this.translateValues, deleteConfirmationText: this.deleteConfirmationText, additionalChecks: this.additionalChecks, + entitySummaryTitle: this.entitySummaryTitle, + entitySummary: this.entitySummary, actionType: this.actionType, buttonType: this.buttonType, delete: this.delete, diff --git a/src/main/webapp/app/shared/delete-dialog/delete-dialog.component.html b/src/main/webapp/app/shared/delete-dialog/delete-dialog.component.html index eaadd2bb1acd..416f30c23c9a 100644 --- a/src/main/webapp/app/shared/delete-dialog/delete-dialog.component.html +++ b/src/main/webapp/app/shared/delete-dialog/delete-dialog.component.html @@ -29,6 +29,21 @@