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 25ba7982aa8c..575f34619666 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 @@ -191,6 +191,7 @@ public ResponseEntity createExam(@PathVariable Long courseId, @RequestBody @EnforceAtLeastInstructor public ResponseEntity updateExam(@PathVariable Long courseId, @RequestBody Exam updatedExam) throws URISyntaxException { log.debug("REST request to update an exam : {}", updatedExam); + if (updatedExam.getId() == null) { return createExam(courseId, updatedExam); } @@ -201,6 +202,7 @@ public ResponseEntity updateExam(@PathVariable Long courseId, @RequestBody // Make sure that the original references are preserved. Exam originalExam = examRepository.findByIdElseThrow(updatedExam.getId()); + var originalExamDuration = originalExam.getDuration(); // The Exam Mode cannot be changed after creation -> Compare request with version in the database if (updatedExam.isTestExam() != originalExam.isTestExam()) { @@ -216,21 +218,22 @@ public ResponseEntity updateExam(@PathVariable Long courseId, @RequestBody Exam savedExam = examRepository.save(updatedExam); + // NOTE: We have to get exercises and groups as we need them for re-scheduling + Exam examWithExercises = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(savedExam.getId()); + // We can't test dates for equality as the dates retrieved from the database lose precision. Also use instant to take timezones into account Comparator comparator = Comparator.comparing(date -> date.truncatedTo(ChronoUnit.SECONDS).toInstant()); if (comparator.compare(originalExam.getVisibleDate(), updatedExam.getVisibleDate()) != 0 || comparator.compare(originalExam.getStartDate(), updatedExam.getStartDate()) != 0) { - // get all exercises - Exam examWithExercises = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(savedExam.getId()); // for all programming exercises in the exam, send their ids for scheduling examWithExercises.getExerciseGroups().stream().flatMap(group -> group.getExercises().stream()).filter(ProgrammingExercise.class::isInstance).map(Exercise::getId) .forEach(instanceMessageSendService::sendProgrammingExerciseSchedule); } - if (comparator.compare(originalExam.getEndDate(), updatedExam.getEndDate()) != 0) { - // get all exercises - Exam examWithExercises = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(savedExam.getId()); - examService.scheduleModelingExercises(examWithExercises); + // NOTE: if the end date was changed, we need to update student exams and re-schedule exercises + if (!originalExam.getEndDate().equals(savedExam.getEndDate())) { + int workingTimeChange = savedExam.getDuration() - originalExamDuration; + updateStudentExamsAndRescheduleExercises(examWithExercises, originalExamDuration, workingTimeChange); } if (updatedChannel != null) { @@ -259,9 +262,7 @@ public ResponseEntity updateExamWorkingTime(@PathVariable Long courseId, @ throw new BadRequestException(); } - var now = now(); - - // We have to get exercise groups as `scheduleModelingExercises` needs them + // NOTE: We have to get exercise groups as `scheduleModelingExercises` needs them Exam exam = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(examId); var originalExamDuration = exam.getDuration(); @@ -270,10 +271,18 @@ public ResponseEntity updateExamWorkingTime(@PathVariable Long courseId, @ exam.setWorkingTime(exam.getWorkingTime() + workingTimeChange); examRepository.save(exam); + // 2. Re-calculate the working times of all student exams + updateStudentExamsAndRescheduleExercises(exam, originalExamDuration, workingTimeChange); + + return ResponseEntity.ok(exam); + } + + private void updateStudentExamsAndRescheduleExercises(Exam exam, Integer originalExamDuration, Integer workingTimeChange) { + var now = now(); + User instructor = userRepository.getUser(); - // 2. Re-calculate the working times of all student exams - var studentExams = studentExamRepository.findByExamId(examId); + var studentExams = studentExamRepository.findByExamId(exam.getId()); for (var studentExam : studentExams) { Integer originalStudentWorkingTime = studentExam.getWorkingTime(); int originalTimeExtension = originalStudentWorkingTime - originalExamDuration; @@ -288,7 +297,9 @@ public ResponseEntity updateExamWorkingTime(@PathVariable Long courseId, @ int adjustedWorkingTime = Math.max(newNormalWorkingTime + timeAdjustment, 0); studentExam.setWorkingTime(adjustedWorkingTime); } + // TODO: probably batch these updates? var savedStudentExam = studentExamRepository.save(studentExam); + // NOTE: if the exam is already visible, notify the student about the working time change if (now.isAfter(exam.getVisibleDate())) { examLiveEventsService.createAndSendWorkingTimeUpdateEvent(savedStudentExam, savedStudentExam.getWorkingTime(), originalStudentWorkingTime, true, instructor); @@ -300,12 +311,10 @@ public ResponseEntity updateExamWorkingTime(@PathVariable Long courseId, @ instanceMessageSendService.sendRescheduleAllStudentExams(exam.getId()); } + // NOTE: potentially re-schedule clustering of modeling submissions (in case Compass is active) if (now.isBefore(examDateService.getLatestIndividualExamEndDate(exam))) { - // potentially re-schedule clustering of modeling submissions (in case Compass is active) examService.scheduleModelingExercises(exam); } - - return ResponseEntity.ok(exam); } /** diff --git a/src/main/webapp/app/exam/manage/exam-management-resolve.service.ts b/src/main/webapp/app/exam/manage/exam-management-resolve.service.ts index 0b35c150992e..49a8462bc98d 100644 --- a/src/main/webapp/app/exam/manage/exam-management-resolve.service.ts +++ b/src/main/webapp/app/exam/manage/exam-management-resolve.service.ts @@ -8,6 +8,27 @@ import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; import { Observable, filter, map, of } from 'rxjs'; import { HttpResponse } from '@angular/common/http'; import { StudentExamWithGradeDTO } from 'app/exam/exam-scores/exam-score-dtos.model'; +import { Course } from 'app/entities/course.model'; +import { CourseManagementService } from 'app/course/manage/course-management.service'; +import { catchError } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class CourseResolve implements Resolve { + constructor(private courseManagementService: CourseManagementService) {} + + resolve(route: ActivatedRouteSnapshot): Observable { + const courseId = route.params['courseId']; + + if (courseId) { + return this.courseManagementService.find(courseId).pipe( + map((response) => response.body), + catchError(() => of(null)), + ); + } + + return of(null); + } +} @Injectable({ providedIn: 'root' }) export class ExamResolve implements Resolve { diff --git a/src/main/webapp/app/exam/manage/exam-management.route.ts b/src/main/webapp/app/exam/manage/exam-management.route.ts index cf7b396060f0..5d9b09e74882 100644 --- a/src/main/webapp/app/exam/manage/exam-management.route.ts +++ b/src/main/webapp/app/exam/manage/exam-management.route.ts @@ -53,7 +53,7 @@ import { OrionTutorAssessmentComponent } from 'app/orion/assessment/orion-tutor- import { isOrion } from 'app/shared/orion/orion'; import { FileUploadExerciseManagementResolve } from 'app/exercises/file-upload/manage/file-upload-exercise-management-resolve.service'; import { ModelingExerciseResolver } from 'app/exercises/modeling/manage/modeling-exercise-resolver.service'; -import { ExamResolve, ExerciseGroupResolve, StudentExamResolve } from 'app/exam/manage/exam-management-resolve.service'; +import { CourseResolve, ExamResolve, ExerciseGroupResolve, StudentExamResolve } from 'app/exam/manage/exam-management-resolve.service'; import { BonusComponent } from 'app/grading-system/bonus/bonus.component'; import { SuspiciousBehaviorComponent } from 'app/exam/manage/suspicious-behavior/suspicious-behavior.component'; import { SuspiciousSessionsOverviewComponent } from 'app/exam/manage/suspicious-behavior/suspicious-sessions-overview/suspicious-sessions-overview.component'; @@ -73,6 +73,7 @@ export const examManagementRoute: Routes = [ component: ExamUpdateComponent, resolve: { exam: ExamResolve, + course: CourseResolve, }, data: { authorities: [Authority.INSTRUCTOR, Authority.ADMIN], @@ -85,6 +86,7 @@ export const examManagementRoute: Routes = [ component: ExamUpdateComponent, resolve: { exam: ExamResolve, + course: CourseResolve, }, data: { authorities: [Authority.INSTRUCTOR, Authority.ADMIN], @@ -113,6 +115,7 @@ export const examManagementRoute: Routes = [ component: ExamUpdateComponent, resolve: { exam: ExamResolve, + course: CourseResolve, }, data: { authorities: [Authority.INSTRUCTOR, Authority.ADMIN], diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component.html index 36e7d8210cbb..f1847f493456 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component.html @@ -11,16 +11,14 @@