Skip to content

Commit

Permalink
Exam mode: Add summary to exam deletion dialog (#9185)
Browse files Browse the repository at this point in the history
  • Loading branch information
ole-ve authored Oct 12, 2024
1 parent edf208e commit 491e368
Show file tree
Hide file tree
Showing 33 changed files with 703 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ public void addTag(String tag) {
this.tags.add(tag);
}

public void setCourse(Course course) {
this.course = course;
}

public Conversation getConversation() {
return conversation;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Long> postIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@ default Post findPostByIdElseThrow(Long postId) throws EntityNotFoundException {
default Post findPostOrMessagePostByIdElseThrow(Long postId) throws EntityNotFoundException {
return getValueElseThrow(findById(postId), postId);
}

List<Post> findAllByConversationId(Long conversationId);

List<Post> findAllByCourseId(Long courseId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.tum.cit.aet.artemis.core.dto;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record CourseDeletionSummaryDTO(long numberOfBuilds, long numberOfCommunicationPosts, long numberOfAnswerPosts) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,20 @@
import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository;
import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService;
import de.tum.cit.aet.artemis.communication.domain.NotificationType;
import de.tum.cit.aet.artemis.communication.domain.Post;
import de.tum.cit.aet.artemis.communication.domain.notification.GroupNotification;
import de.tum.cit.aet.artemis.communication.repository.AnswerPostRepository;
import de.tum.cit.aet.artemis.communication.repository.FaqRepository;
import de.tum.cit.aet.artemis.communication.repository.GroupNotificationRepository;
import de.tum.cit.aet.artemis.communication.repository.PostRepository;
import de.tum.cit.aet.artemis.communication.repository.conversation.ConversationRepository;
import de.tum.cit.aet.artemis.communication.service.notifications.GroupNotificationService;
import de.tum.cit.aet.artemis.core.config.Constants;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.DomainObject;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.dto.CourseContentCount;
import de.tum.cit.aet.artemis.core.dto.CourseDeletionSummaryDTO;
import de.tum.cit.aet.artemis.core.dto.CourseManagementDetailViewDTO;
import de.tum.cit.aet.artemis.core.dto.DueDateStat;
import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO;
Expand Down Expand Up @@ -104,6 +108,7 @@
import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase;
import de.tum.cit.aet.artemis.plagiarism.repository.PlagiarismCaseRepository;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.repository.BuildJobRepository;
import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository;
import de.tum.cit.aet.artemis.tutorialgroup.repository.TutorialGroupNotificationRepository;
import de.tum.cit.aet.artemis.tutorialgroup.repository.TutorialGroupRepository;
Expand Down Expand Up @@ -201,6 +206,12 @@ public class CourseService {

private final TutorialGroupNotificationRepository tutorialGroupNotificationRepository;

private final PostRepository postRepository;

private final AnswerPostRepository answerPostRepository;

private final BuildJobRepository buildJobRepository;

public CourseService(CourseRepository courseRepository, ExerciseService exerciseService, ExerciseDeletionService exerciseDeletionService,
AuthorizationCheckService authCheckService, UserRepository userRepository, LectureService lectureService, GroupNotificationRepository groupNotificationRepository,
ExerciseGroupRepository exerciseGroupRepository, AuditEventRepository auditEventRepository, UserService userService, ExamDeletionService examDeletionService,
Expand All @@ -213,7 +224,8 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise
TutorialGroupRepository tutorialGroupRepository, PlagiarismCaseRepository plagiarismCaseRepository, ConversationRepository conversationRepository,
LearningPathService learningPathService, Optional<IrisSettingsService> irisSettingsService, LectureRepository lectureRepository,
TutorialGroupNotificationRepository tutorialGroupNotificationRepository, TutorialGroupChannelManagementService tutorialGroupChannelManagementService,
PrerequisiteRepository prerequisiteRepository, CompetencyRelationRepository competencyRelationRepository, FaqRepository faqRepository) {
PrerequisiteRepository prerequisiteRepository, CompetencyRelationRepository competencyRelationRepository, PostRepository postRepository,
AnswerPostRepository answerPostRepository, BuildJobRepository buildJobRepository, FaqRepository faqRepository) {
this.courseRepository = courseRepository;
this.exerciseService = exerciseService;
this.exerciseDeletionService = exerciseDeletionService;
Expand Down Expand Up @@ -253,6 +265,9 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise
this.tutorialGroupChannelManagementService = tutorialGroupChannelManagementService;
this.prerequisiteRepository = prerequisiteRepository;
this.competencyRelationRepository = competencyRelationRepository;
this.buildJobRepository = buildJobRepository;
this.postRepository = postRepository;
this.answerPostRepository = answerPostRepository;
this.faqRepository = faqRepository;
}

Expand Down Expand Up @@ -444,6 +459,22 @@ public Set<Course> findAllOnlineCoursesForPlatformForUser(String registrationId,
.collect(Collectors.toSet());
}

/**
* Get the course deletion summary for the given course.
*
* @param course the course for which to get the deletion summary
* @return the course deletion summary
*/
public CourseDeletionSummaryDTO getDeletionSummary(Course course) {
List<Long> programmingExerciseIds = course.getExercises().stream().map(Exercise::getId).toList();
long numberOfBuilds = buildJobRepository.countBuildJobsByExerciseIds(programmingExerciseIds);

List<Post> posts = postRepository.findAllByCourseId(course.getId());
long numberOfCommunicationPosts = posts.size();
long numberOfAnswerPosts = answerPostRepository.countAnswerPostsByPostIdIn(posts.stream().map(Post::getId).toList());
return new CourseDeletionSummaryDTO(numberOfBuilds, numberOfCommunicationPosts, numberOfAnswerPosts);
}

/**
* Deletes all elements associated with the course including:
* <ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import de.tum.cit.aet.artemis.core.config.Constants;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.dto.CourseDeletionSummaryDTO;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.CourseRepository;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
Expand Down Expand Up @@ -181,6 +182,21 @@ public ResponseEntity<Void> deleteCourse(@PathVariable long courseId) {
return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, Course.ENTITY_NAME, course.getTitle())).build();
}

/**
* GET /courses/:courseId/deletion-summary : get the deletion summary for the course with the given id.
*
* @param courseId the id of the course
* @return the ResponseEntity with status 200 (OK) and the deletion summary in the body
*/
@GetMapping("courses/{courseId}/deletion-summary")
@EnforceAdmin
public ResponseEntity<CourseDeletionSummaryDTO> getDeletionSummary(@PathVariable long courseId) {
log.debug("REST request to get deletion summary course: {}", courseId);
final Course course = courseRepository.findByIdWithEagerExercisesElseThrow(courseId);

return ResponseEntity.ok().body(courseService.getDeletionSummary(course));
}

/**
* Creates a default channel with the given name and adds all students, tutors and instructors as participants.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package de.tum.cit.aet.artemis.exam.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) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ SELECT MAX(se.workingTime)
""")
Set<StudentExam> findAllUnsubmittedWithExercisesByExamId(@Param("examId") Long examId);

List<StudentExam> findAllByExamId(Long examId);

List<StudentExam> findAllByExamId_AndTestRunIsTrue(Long examId);

@Query("""
SELECT DISTINCT se
FROM StudentExam se
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

import de.tum.cit.aet.artemis.assessment.domain.GradingScale;
import de.tum.cit.aet.artemis.assessment.repository.GradingScaleRepository;
import de.tum.cit.aet.artemis.communication.domain.Post;
import de.tum.cit.aet.artemis.communication.domain.conversation.Channel;
import de.tum.cit.aet.artemis.communication.repository.AnswerPostRepository;
import de.tum.cit.aet.artemis.communication.repository.PostRepository;
import de.tum.cit.aet.artemis.communication.repository.conversation.ChannelRepository;
import de.tum.cit.aet.artemis.communication.service.conversation.ChannelService;
import de.tum.cit.aet.artemis.core.config.Constants;
Expand All @@ -29,13 +32,16 @@
import de.tum.cit.aet.artemis.exam.domain.Exam;
import de.tum.cit.aet.artemis.exam.domain.ExerciseGroup;
import de.tum.cit.aet.artemis.exam.domain.StudentExam;
import de.tum.cit.aet.artemis.exam.dto.ExamDeletionSummaryDTO;
import de.tum.cit.aet.artemis.exam.repository.ExamLiveEventRepository;
import de.tum.cit.aet.artemis.exam.repository.ExamRepository;
import de.tum.cit.aet.artemis.exam.repository.StudentExamRepository;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.domain.ExerciseType;
import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository;
import de.tum.cit.aet.artemis.exercise.service.ExerciseDeletionService;
import de.tum.cit.aet.artemis.exercise.service.ParticipationService;
import de.tum.cit.aet.artemis.programming.repository.BuildJobRepository;
import de.tum.cit.aet.artemis.quiz.domain.QuizPool;
import de.tum.cit.aet.artemis.quiz.repository.QuizPoolRepository;

Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -240,4 +256,34 @@ 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 = exam.getExerciseGroups().stream().flatMap(group -> group.getExercises().stream())
.filter(exercise -> ExerciseType.PROGRAMMING.equals(exercise.getExerciseType()))
.mapToLong(exercise -> buildJobRepository.countBuildJobsByExerciseIds(List.of(exercise.getId()))).sum();

Channel channel = channelRepository.findChannelByExamId(examId);
Long conversationId = channel.getId();

List<Long> postIds = postRepository.findAllByConversationId(conversationId).stream().map(Post::getId).toList();
long numberOfCommunicationPosts = postIds.size();
long numberOfAnswerPosts = answerPostRepository.countAnswerPostsByPostIdIn(postIds);

Set<StudentExam> studentExams = exam.getStudentExams();
long numberRegisteredStudents = studentExams.size();

// Boolean.TRUE/Boolean.FALSE are used to handle the case where isStarted/isSubmitted is null
long notStartedExams = studentExams.stream().filter(studentExam -> studentExam.isStarted() == null || !studentExam.isStarted()).count();
long startedExams = studentExams.stream().filter(studentExam -> Boolean.TRUE.equals(studentExam.isStarted())).count();
long submittedExams = studentExams.stream().filter(studentExam -> Boolean.TRUE.equals(studentExam.isStarted()) && Boolean.TRUE.equals(studentExam.isSubmitted())).count();

return new ExamDeletionSummaryDTO(numberOfBuilds, numberOfCommunicationPosts, numberOfAnswerPosts, numberRegisteredStudents, notStartedExams, startedExams, submittedExams);
}
}
17 changes: 17 additions & 0 deletions src/main/java/de/tum/cit/aet/artemis/exam/web/ExamResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor;
import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastInstructorInCourse;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.core.service.feature.Feature;
import de.tum.cit.aet.artemis.core.service.feature.FeatureToggle;
Expand All @@ -88,6 +89,7 @@
import de.tum.cit.aet.artemis.exam.domain.StudentExam;
import de.tum.cit.aet.artemis.exam.domain.SuspiciousSessionsAnalysisOptions;
import de.tum.cit.aet.artemis.exam.dto.ExamChecklistDTO;
import de.tum.cit.aet.artemis.exam.dto.ExamDeletionSummaryDTO;
import de.tum.cit.aet.artemis.exam.dto.ExamInformationDTO;
import de.tum.cit.aet.artemis.exam.dto.ExamScoresDTO;
import de.tum.cit.aet.artemis.exam.dto.ExamUserDTO;
Expand Down Expand Up @@ -1320,4 +1322,19 @@ public ResponseEntity<Set<SuspiciousExamSessionsDTO>> getAllSuspiciousExamSessio
analyzeSessionsIpOutsideOfRange);
return ResponseEntity.ok(examSessionService.retrieveAllSuspiciousExamSessionsByExamId(examId, options, Optional.ofNullable(ipSubnet)));
}

/**
* GET /courses/{courseId}/exams/{examId}/deletion-summary : Get a summary of the deletion of an exam.
*
* @param courseId the id of the course
* @param examId the id of the exam
*
* @return the ResponseEntity with status 200 (OK) and with body a summary of the deletion of the exam
*/
@GetMapping("courses/{courseId}/exams/{examId}/deletion-summary")
@EnforceAtLeastInstructorInCourse
public ResponseEntity<ExamDeletionSummaryDTO> getDeletionSummary(@PathVariable long courseId, @PathVariable long examId) {
log.debug("REST request to get deletion summary for exam : {}", examId);
return ResponseEntity.ok(examDeletionService.getExamDeletionSummary(examId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,19 @@ default BuildJob findByBuildJobIdElseThrow(String buildJobId) {
return getValueElseThrow(findByBuildJobId(buildJobId));
}

/**
* Get the number of build jobs for a list of exercise ids.
*
* @param exerciseIds the list of exercise ids
* @return the number of build jobs
*/
@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 IN :exerciseIds
""")
long countBuildJobsByExerciseIds(@Param("exerciseIds") List<Long> exerciseIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ Optional<ProgrammingExercise> findWithTemplateAndSolutionParticipationTeamAssign

List<ProgrammingExercise> findAllByProjectKey(String projectKey);

List<ProgrammingExercise> findAllByCourseId(Long courseId);

@EntityGraph(type = LOAD, attributePaths = "submissionPolicy")
List<ProgrammingExercise> findWithSubmissionPolicyByProjectKey(String projectKey);

Expand Down
9 changes: 9 additions & 0 deletions src/main/webapp/app/course/manage/course-admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { map } from 'rxjs/operators';
import { Course } from 'app/entities/course.model';
import { objectToJsonBlob } from 'app/utils/blob-util';
import { CourseManagementService } from 'app/course/manage/course-management.service';
import { CourseDeletionSummaryDTO } from 'app/entities/course-deletion-summary.model';

export type EntityResponseType = HttpResponse<Course>;
export type EntityArrayResponseType = HttpResponse<Course[]>;
Expand Down Expand Up @@ -51,4 +52,12 @@ export class CourseAdminService {
delete(courseId: number): Observable<HttpResponse<void>> {
return this.http.delete<void>(`${this.resourceUrl}/${courseId}`, { observe: 'response' });
}

/**
* Returns a summary for the course providing information potentially relevant for the deletion.
* @param courseId - the id of the course to get the deletion summary for
*/
getDeletionSummary(courseId: number): Observable<HttpResponse<CourseDeletionSummaryDTO>> {
return this.http.get<CourseDeletionSummaryDTO>(`${this.resourceUrl}/${courseId}/deletion-summary`, { observe: 'response' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
[buttonSize]="ButtonSize.MEDIUM"
jhiDeleteButton
[entityTitle]="course.title || ''"
entitySummaryTitle="artemisApp.course.delete.summary.title"
[fetchEntitySummary]="fetchCourseDeletionSummary()"
deleteQuestion="artemisApp.course.delete.question"
deleteConfirmationText="artemisApp.course.delete.typeNameToConfirm"
(delete)="deleteCourse(course.id!)"
Expand Down
Loading

0 comments on commit 491e368

Please sign in to comment.