Skip to content

Commit

Permalink
Merge branch 'develop' into development/fix-PostIntegrationTest
Browse files Browse the repository at this point in the history
  • Loading branch information
laadvo authored Sep 20, 2023
2 parents c17659b + c5bf576 commit 700da16
Show file tree
Hide file tree
Showing 21 changed files with 186 additions and 77 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ plugins {
}

group = "de.tum.in.www1.artemis"
version = "6.5.0"
version = "6.5.1"
description = "Interactive Learning with Individual Feedback"

sourceCompatibility=17
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ module.exports = {
statements: 85.6,
branches: 72.8,
functions: 79.4,
lines: 85.9,
lines: 85.8,
},
},
coverageReporters: ["clover", "json", "lcov", "text-summary"],
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "artemis",
"version": "6.5.0",
"version": "6.5.1",
"description": "Interactive Learning with Individual Feedback",
"private": true,
"license": "MIT",
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/config/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ public final class Constants {

public static final String UNENROLL_FROM_COURSE = "UNENROLL_FROM_COURSE";

public static final String CLEANUP_COURSE = "CLEANUP_COURSE";

public static final String CLEANUP_EXAM = "CLEANUP_EXAM";

public static final String DELETE_EXERCISE = "DELETE_EXERCISE";

public static final String EDIT_EXERCISE = "EDIT_EXERCISE";
Expand Down
18 changes: 10 additions & 8 deletions src/main/java/de/tum/in/www1/artemis/service/CourseService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Principal;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.ZoneId;
Expand Down Expand Up @@ -782,30 +783,31 @@ public void archiveCourse(Course course) {

/**
* Cleans up a course by cleaning up all exercises from that course. This deletes all student
* submissions. Note that a course has to be archived first before being cleaned up.
* repositories and build plans. Note that a course has to be archived first before being cleaned up.
*
* @param courseId The id of the course to clean up
* @param courseId The id of the course to clean up
* @param principal the user that wants to cleanup the course
*/
public void cleanupCourse(Long courseId) {
public void cleanupCourse(Long courseId, Principal principal) {
final var auditEvent = new AuditEvent(principal.getName(), Constants.CLEANUP_COURSE, "course=" + courseId);
auditEventRepository.add(auditEvent);
// Get the course with all exercises
var course = courseRepository.findByIdWithExercisesAndLecturesElseThrow(courseId);
var course = courseRepository.findByIdWithEagerExercisesElseThrow(courseId);
if (!course.hasCourseArchive()) {
log.info("Cannot clean up course {} because it hasn't been archived.", courseId);
return;
}

// The Objects::nonNull is needed here because the relationship exam -> exercise groups is ordered and
// hibernate sometimes adds nulls to in the list of exercise groups to keep the order
Set<Exercise> examExercises = examRepository.findByCourseIdWithExerciseGroupsAndExercises(courseId).stream().map(Exam::getExerciseGroups).flatMap(Collection::stream)
// hibernate sometimes adds nulls into the list of exercise groups to keep the order
Set<Exercise> examExercises = examRepository.findByCourseIdWithExerciseGroupsAndExercises(courseId).stream().flatMap(e -> e.getExerciseGroups().stream())
.filter(Objects::nonNull).map(ExerciseGroup::getExercises).flatMap(Collection::stream).collect(Collectors.toSet());

var exercisesToCleanup = Stream.concat(course.getExercises().stream(), examExercises.stream()).collect(Collectors.toSet());
exercisesToCleanup.forEach(exercise -> {
if (exercise instanceof ProgrammingExercise) {
exerciseDeletionService.cleanup(exercise.getId(), true);
}

// TODO: extend exerciseDeletionService.cleanup to clean up all exercise types
});

log.info("The course {} has been cleaned up!", courseId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -16,7 +18,6 @@
import de.tum.in.www1.artemis.domain.metis.conversation.Channel;
import de.tum.in.www1.artemis.domain.modeling.ModelingExercise;
import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.domain.quiz.QuizExercise;
import de.tum.in.www1.artemis.repository.*;
import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository;
Expand Down Expand Up @@ -89,26 +90,29 @@ public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUn
* @param deleteRepositories if true, the repositories gets deleted
*/
public void cleanup(Long exerciseId, boolean deleteRepositories) {
log.info("Cleanup all participations for exercise {} in parallel", exerciseId);
Exercise exercise = exerciseRepository.findByIdWithStudentParticipationsElseThrow(exerciseId);
log.info("Request to cleanup all participations for Exercise : {}", exercise.getTitle());
if (!(exercise instanceof ProgrammingExercise)) {
log.warn("Exercise with exerciseId {} is not an instance of ProgrammingExercise. Ignoring the request to cleanup repositories and build plan", exerciseId);
return;
}

if (exercise instanceof ProgrammingExercise) {
for (StudentParticipation participation : exercise.getStudentParticipations()) {
// Cleanup in parallel to speedup the process
var threadPool = Executors.newFixedThreadPool(10);
var futures = exercise.getStudentParticipations().stream().map(participation -> CompletableFuture.runAsync(() -> {
try {
participationService.cleanupBuildPlan((ProgrammingExerciseStudentParticipation) participation);
}

if (!deleteRepositories) {
return; // in this case, we are done
}

for (StudentParticipation participation : exercise.getStudentParticipations()) {
if (!deleteRepositories) {
return; // in this case, we are done with the participation
}
participationService.cleanupRepository((ProgrammingExerciseStudentParticipation) participation);
}

}
else {
log.warn("Exercise with exerciseId {} is not an instance of ProgrammingExercise. Ignoring the request to cleanup repositories and build plan", exerciseId);
}
catch (Exception exception) {
log.error("Failed to clean the student participation {} for programming exercise {}", participation.getId(), exerciseId);
}
}, threadPool).toCompletableFuture()).toArray(CompletableFuture[]::new);
// wait until all operations finish before returning
CompletableFuture.allOf(futures).thenRun(threadPool::shutdown).join();
}

/**
Expand Down
39 changes: 37 additions & 2 deletions src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Principal;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Collectors;
Expand All @@ -16,13 +17,16 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.databind.ObjectMapper;

import de.tum.in.www1.artemis.config.Constants;
import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.enumeration.*;
import de.tum.in.www1.artemis.domain.exam.Exam;
Expand Down Expand Up @@ -107,8 +111,12 @@ public class ExamService {

private final BonusService bonusService;

private final ExerciseDeletionService exerciseDeletionService;

private final SubmittedAnswerRepository submittedAnswerRepository;

private final AuditEventRepository auditEventRepository;

private final CourseScoreCalculationService courseScoreCalculationService;

private final CourseRepository courseRepository;
Expand All @@ -121,8 +129,8 @@ public ExamService(ExamRepository examRepository, StudentExamRepository studentE
ProgrammingExerciseRepository programmingExerciseRepository, QuizExerciseRepository quizExerciseRepository, ResultRepository resultRepository,
SubmissionRepository submissionRepository, CourseExamExportService courseExamExportService, GitService gitService, GroupNotificationService groupNotificationService,
GradingScaleRepository gradingScaleRepository, PlagiarismCaseRepository plagiarismCaseRepository, AuthorizationCheckService authorizationCheckService,
BonusService bonusService, SubmittedAnswerRepository submittedAnswerRepository, CourseScoreCalculationService courseScoreCalculationService,
CourseRepository courseRepository) {
BonusService bonusService, ExerciseDeletionService exerciseDeletionService, SubmittedAnswerRepository submittedAnswerRepository,
AuditEventRepository auditEventRepository, CourseScoreCalculationService courseScoreCalculationService, CourseRepository courseRepository) {
this.examRepository = examRepository;
this.studentExamRepository = studentExamRepository;
this.userRepository = userRepository;
Expand All @@ -143,7 +151,9 @@ public ExamService(ExamRepository examRepository, StudentExamRepository studentE
this.plagiarismCaseRepository = plagiarismCaseRepository;
this.authorizationCheckService = authorizationCheckService;
this.bonusService = bonusService;
this.exerciseDeletionService = exerciseDeletionService;
this.submittedAnswerRepository = submittedAnswerRepository;
this.auditEventRepository = auditEventRepository;
this.courseScoreCalculationService = courseScoreCalculationService;
this.courseRepository = courseRepository;
this.defaultObjectMapper = new ObjectMapper();
Expand Down Expand Up @@ -1307,6 +1317,31 @@ public Page<Exam> getAllActiveExams(final Pageable pageable, final User user) {
ZonedDateTime.now().plusDays(EXAM_ACTIVE_DAYS));
}

/**
* Cleans up an exam by cleaning up all exercises from that course. This deletes all student
* repositories and build plans. Note that an exam has to be archived first before being cleaned up.
*
* @param examId The id of the exam to clean up
* @param principal the user that wants to cleanup the exam
*/
public void cleanupExam(Long examId, Principal principal) {
final var auditEvent = new AuditEvent(principal.getName(), Constants.CLEANUP_EXAM, "exam=" + examId);
auditEventRepository.add(auditEvent);

// The Objects::nonNull is needed here because the relationship exam -> exercise groups is ordered and
// hibernate sometimes adds nulls into the list of exercise groups to keep the order
Set<Exercise> examExercises = examRepository.findByIdWithExamUsersExerciseGroupsAndExercisesElseThrow(examId).getExerciseGroups().stream().filter(Objects::nonNull)
.map(ExerciseGroup::getExercises).flatMap(Collection::stream).collect(Collectors.toSet());

examExercises.forEach(exercise -> {
if (exercise instanceof ProgrammingExercise) {
exerciseDeletionService.cleanup(exercise.getId(), true);
}
});

log.info("The exam {} has been cleaned up!", examId);
}

/**
* A specialized BiFunction<Long, Double, BonusResultDTO> functional interface to provide a simple interface
* for passing the data dependencies needed for a Bonus calculation (like source course/exam results).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@
import de.tum.in.www1.artemis.service.*;
import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseParticipationService;
import de.tum.in.www1.artemis.service.programming.ProgrammingTriggerService;
import de.tum.in.www1.artemis.service.scheduled.ProgrammingExerciseScheduleService;
import de.tum.in.www1.artemis.service.util.ExamExerciseStartPreparationStatus;
import de.tum.in.www1.artemis.service.util.Tuple;
import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;

Expand Down Expand Up @@ -89,8 +87,6 @@ public class StudentExamService {

private final WebsocketMessagingService websocketMessagingService;

private final ProgrammingExerciseScheduleService programmingExerciseScheduleService;

private final TaskScheduler scheduler;

public StudentExamService(StudentExamRepository studentExamRepository, UserRepository userRepository, ParticipationService participationService,
Expand All @@ -99,7 +95,7 @@ public StudentExamService(StudentExamRepository studentExamRepository, UserRepos
ProgrammingExerciseParticipationService programmingExerciseParticipationService, SubmissionService submissionService,
StudentParticipationRepository studentParticipationRepository, ExamQuizService examQuizService, ProgrammingExerciseRepository programmingExerciseRepository,
ProgrammingTriggerService programmingTriggerService, ExamRepository examRepository, CacheManager cacheManager, WebsocketMessagingService websocketMessagingService,
ProgrammingExerciseScheduleService programmingExerciseScheduleService, @Qualifier("taskScheduler") TaskScheduler scheduler) {
@Qualifier("taskScheduler") TaskScheduler scheduler) {
this.participationService = participationService;
this.studentExamRepository = studentExamRepository;
this.userRepository = userRepository;
Expand All @@ -117,7 +113,6 @@ public StudentExamService(StudentExamRepository studentExamRepository, UserRepos
this.examRepository = examRepository;
this.cacheManager = cacheManager;
this.websocketMessagingService = websocketMessagingService;
this.programmingExerciseScheduleService = programmingExerciseScheduleService;
this.scheduler = scheduler;
}

Expand Down Expand Up @@ -654,8 +649,9 @@ private void setUpExerciseParticipationsAndSubmissionsWithInitializationDate(Stu
// Normally, the locking operation at the end of the exam gets scheduled during the initial unlocking process
// (see ProgrammingExerciseScheduleService#scheduleIndividualRepositoryAndParticipationLockTasks)
// Since this gets never executed here, we need to manually schedule the locking.
var tupel = new Tuple<>(studentExam.getIndividualEndDate(), programmingParticipation);
programmingExerciseScheduleService.scheduleIndividualRepositoryAndParticipationLockTasks(programmingExercise, Set.of(tupel));
// TODO: reconsider this edge case and instead send a message to over the instanceMessageSendService
// var tupel = new Tuple<>(studentExam.getIndividualEndDate(), programmingParticipation);
// programmingExerciseScheduleService.scheduleIndividualRepositoryAndParticipationLockTasks(programmingExercise, Set.of(tupel));
}
else {
programmingExerciseParticipationService.lockStudentParticipation(programmingParticipation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ public Runnable unlockAllStudentParticipationsWithEarlierStartDateAndLaterDueDat
* @param exercise the programming exercise for which the lock is executed
* @param individualParticipationsWithDueDates the set of student participations with their individual due dates
*/
public void scheduleIndividualRepositoryAndParticipationLockTasks(ProgrammingExercise exercise,
private void scheduleIndividualRepositoryAndParticipationLockTasks(ProgrammingExercise exercise,
Set<Tuple<ZonedDateTime, ProgrammingExerciseStudentParticipation>> individualParticipationsWithDueDates) {
// 1. Group all participations by due date
// TODO: use student exams for safety if some participations are not pre-generated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ public ResponseEntity<Long> getNumberOfAllowedComplaintsInCourse(@PathVariable L
* exercises
*
* @param exerciseId the id of the exercise we are interested in
* @param principal that wants to get complaints
* @param principal the user that wants to get complaints
* @return the ResponseEntity with status 200 (OK) and a list of complaints. The list can be empty
*/
@GetMapping("exercises/{exerciseId}/complaints-for-test-run-dashboard")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.file.Path;
import java.security.Principal;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -769,20 +770,21 @@ public ResponseEntity<Resource> downloadCourseArchive(@PathVariable Long courseI
/**
* DELETE /courses/:course/cleanup : Cleans up a course by deleting all student submissions.
*
* @param courseId id of the course to clean up
* @param courseId id of the course to clean up
* @param principal the user that wants to cleanup the course
* @return ResponseEntity with status
*/
@DeleteMapping("courses/{courseId}/cleanup")
@EnforceAtLeastInstructor
public ResponseEntity<Resource> cleanup(@PathVariable Long courseId) {
public ResponseEntity<Resource> cleanup(@PathVariable Long courseId, Principal principal) {
log.info("REST request to cleanup the Course : {}", courseId);
final Course course = courseRepository.findByIdElseThrow(courseId);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null);
// Forbid cleaning the course if no archive has been created
if (!course.hasCourseArchive()) {
throw new BadRequestAlertException("Failed to clean up course " + courseId + " because it needs to be archived first.", Course.ENTITY_NAME, "archivenonexistant");
}
courseService.cleanupCourse(courseId);
courseService.cleanupCourse(courseId, principal);
return ResponseEntity.ok().build();
}

Expand Down
Loading

0 comments on commit 700da16

Please sign in to comment.