From 7b0bf9b2c3e9ea98147331407e1a3aa2a2094254 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:23:28 +0200 Subject: [PATCH 01/32] Enable time update in exam edit, add time extension tests --- .../www1/artemis/web/rest/ExamResource.java | 90 ++++++++++++++----- src/main/webapp/i18n/de/exam.json | 2 +- src/main/webapp/i18n/en/exam.json | 2 +- .../exam/ProgrammingExamIntegrationTest.java | 44 ++++++++- ...rogrammingExerciseScheduleServiceTest.java | 45 +++++++++- 5 files changed, 152 insertions(+), 31 deletions(-) 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 fe8898f682a6..c335175cf993 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 @@ -12,7 +12,12 @@ import java.security.Principal; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; import javax.validation.constraints.NotNull; import javax.ws.rs.BadRequestException; @@ -25,24 +30,51 @@ import org.springframework.core.io.Resource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.http.*; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import de.tum.in.www1.artemis.config.Constants; -import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.Submission; +import de.tum.in.www1.artemis.domain.User; 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.conversation.Channel; import de.tum.in.www1.artemis.domain.participation.TutorParticipation; -import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.repository.CourseRepository; +import de.tum.in.www1.artemis.repository.CustomAuditEventRepository; +import de.tum.in.www1.artemis.repository.ExamRepository; +import de.tum.in.www1.artemis.repository.ExerciseRepository; +import de.tum.in.www1.artemis.repository.StudentExamRepository; +import de.tum.in.www1.artemis.repository.TutorParticipationRepository; +import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; import de.tum.in.www1.artemis.security.Role; -import de.tum.in.www1.artemis.security.annotations.*; -import de.tum.in.www1.artemis.service.*; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; +import de.tum.in.www1.artemis.service.AssessmentDashboardService; +import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.ProfileService; +import de.tum.in.www1.artemis.service.SubmissionService; import de.tum.in.www1.artemis.service.dto.StudentDTO; -import de.tum.in.www1.artemis.service.exam.*; +import de.tum.in.www1.artemis.service.exam.ExamAccessService; +import de.tum.in.www1.artemis.service.exam.ExamDateService; +import de.tum.in.www1.artemis.service.exam.ExamDeletionService; +import de.tum.in.www1.artemis.service.exam.ExamImportService; +import de.tum.in.www1.artemis.service.exam.ExamLiveEventsService; +import de.tum.in.www1.artemis.service.exam.ExamRegistrationService; +import de.tum.in.www1.artemis.service.exam.ExamService; +import de.tum.in.www1.artemis.service.exam.ExamSessionService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; import de.tum.in.www1.artemis.service.messaging.InstanceMessageSendService; @@ -50,7 +82,11 @@ import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.dto.*; import de.tum.in.www1.artemis.web.rest.dto.examevent.ExamWideAnnouncementEventDTO; -import de.tum.in.www1.artemis.web.rest.errors.*; +import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenAlertException; +import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; +import de.tum.in.www1.artemis.web.rest.errors.ConflictException; +import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; import io.swagger.annotations.ApiParam; import tech.jhipster.web.util.PaginationUtil; @@ -191,6 +227,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); } @@ -216,21 +253,23 @@ public ResponseEntity updateExam(@PathVariable Long courseId, @RequestBody Exam savedExam = examRepository.save(updatedExam); + // NOTE: We have to get exercise 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 (comparator.compare(originalExam.getEndDate(), savedExam.getEndDate()) != 0) { + // TODO: check if there are any problems with that - i.e. TZ issues, only changing working time in UI, ... + int workingTimeChange = savedExam.getDuration() - originalExam.getDuration(); + updateStudentExamsAndRescheduleExercises(examWithExercises, workingTimeChange); } if (updatedChannel != null) { @@ -259,21 +298,28 @@ 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(); // 1. Update the end date & working time of the exam exam.setEndDate(exam.getEndDate().plusSeconds(workingTimeChange)); exam.setWorkingTime(exam.getWorkingTime() + workingTimeChange); examRepository.save(exam); + // 2. Re-calculate the working times of all student exams + updateStudentExamsAndRescheduleExercises(exam, workingTimeChange); + + return ResponseEntity.ok(exam); + } + + private void updateStudentExamsAndRescheduleExercises(Exam exam, Integer workingTimeChange) { + var originalExamDuration = exam.getDuration(); + + 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; @@ -300,12 +346,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/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index c352956e85d3..8106ac861f66 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -652,7 +652,7 @@ }, "editWorkingTime": { "title": "Prüfungszeit bearbeiten", - "label": "Änderung der Prüfungszeit", + "label": "Änderung der Prüfungszeit (+)", "duration": "Die neue Dauer der Prüfung beträgt {{ duration }}.", "question": "Die Prüfungszeit wird für alle Studierenden angepasst. Wollen Sie die Prüfungszeit für die Klausur {{ title }} wirklich ändern?", "typeNameToConfirm": "Bitte gib den Namen der Klausur zur Bestätigung ein." diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index b18034f4a158..5af043476e7c 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -653,7 +653,7 @@ }, "editWorkingTime": { "title": "Edit Exam Duration", - "label": "Exam duration change", + "label": "Exam duration change (+)", "duration": "The new duration of the exam will be {{ duration }}.", "question": "This will change the working time for all students. Do you really want to edit the duration of the exam {{ title }}?", "typeNameToConfirm": "Please type in the name of the exam to confirm." diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java index e3cfcb57bc23..f82818bb01a7 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ProgrammingExamIntegrationTest.java @@ -2,7 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -36,7 +40,10 @@ import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseTestService; import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; -import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.repository.ExamRepository; +import de.tum.in.www1.artemis.repository.ExerciseRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; +import de.tum.in.www1.artemis.repository.StudentExamRepository; import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.ExamPrepareExercisesTestUtil; @@ -155,6 +162,39 @@ void testUpdateExam_rescheduleProgramming_startDateChanged() throws Exception { verify(instanceMessageSendService).sendProgrammingExerciseSchedule(programmingEx.getId()); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testUpdateExam_rescheduleProgramming_endDateChanged() throws Exception { + var programmingEx = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExerciseAndTestCases(); + var examWithProgrammingEx = programmingEx.getExerciseGroup().getExam(); + + ZonedDateTime visibleDate = examWithProgrammingEx.getVisibleDate(); + ZonedDateTime startDate = examWithProgrammingEx.getStartDate(); + ZonedDateTime endDate = examWithProgrammingEx.getEndDate(); + examUtilService.setVisibleStartAndEndDateOfExam(examWithProgrammingEx, visibleDate, startDate, endDate.plusMinutes(1)); + + request.put("/api/courses/" + examWithProgrammingEx.getCourse().getId() + "/exams", examWithProgrammingEx, HttpStatus.OK); + verify(instanceMessageSendService).sendRescheduleAllStudentExams(examWithProgrammingEx.getId()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testUpdateExamWorkingTime_rescheduleProgramming_endDateChanged() throws Exception { + var programmingEx = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExerciseAndTestCases(); + var examWithProgrammingEx = programmingEx.getExerciseGroup().getExam(); + + var workingTimeExtensionSeconds = 60; + + ZonedDateTime visibleDate = examWithProgrammingEx.getVisibleDate(); + ZonedDateTime startDate = examWithProgrammingEx.getStartDate(); + ZonedDateTime endDate = examWithProgrammingEx.getEndDate(); + examUtilService.setVisibleStartAndEndDateOfExam(examWithProgrammingEx, visibleDate, startDate, endDate); + + request.patch("/api/courses/" + examWithProgrammingEx.getCourse().getId() + "/exams/" + examWithProgrammingEx.getId() + "/working-time", workingTimeExtensionSeconds, + HttpStatus.OK); + verify(instanceMessageSendService).sendRescheduleAllStudentExams(examWithProgrammingEx.getId()); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void lockAllRepositories() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java index 6b546e374c79..c74de5e5e648 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseScheduleServiceTest.java @@ -1,7 +1,15 @@ package de.tum.in.www1.artemis.exercise.programmingexercise; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; @@ -25,7 +33,10 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.VcsRepositoryUrl; -import de.tum.in.www1.artemis.domain.enumeration.*; +import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; +import de.tum.in.www1.artemis.domain.enumeration.ExerciseLifecycle; +import de.tum.in.www1.artemis.domain.enumeration.ParticipationLifecycle; +import de.tum.in.www1.artemis.domain.enumeration.Visibility; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.StudentExam; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; @@ -34,7 +45,11 @@ import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.ExerciseUtilService; import de.tum.in.www1.artemis.participation.ParticipationUtilService; -import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.repository.ExamRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; +import de.tum.in.www1.artemis.repository.StudentExamRepository; import de.tum.in.www1.artemis.service.messaging.InstanceMessageReceiveService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.LocalRepository; @@ -600,7 +615,7 @@ void shouldCancelAllTasksIfSchedulingNoLongerNeeded() throws Exception { @Test @WithMockUser(username = "admin", roles = "ADMIN") - void testExamWorkingTimeChangeDuringConduction() { + void testStudentExamIndividualWorkingTimeChangeDuringConduction() { ProgrammingExercise examExercise = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExercise(); Exam exam = examExercise.getExamViaExerciseGroupOrCourseMember(); exam.setStartDate(ZonedDateTime.now().minusMinutes(1)); @@ -619,6 +634,28 @@ void testExamWorkingTimeChangeDuringConduction() { participation.getStudents()); } + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void testRescheduleExamDuringConduction() { + ProgrammingExercise examExercise = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExercise(); + Exam exam = examExercise.getExamViaExerciseGroupOrCourseMember(); + exam.setStartDate(ZonedDateTime.now().minusMinutes(1)); + exam = examRepository.saveAndFlush(exam); + User user = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + StudentExam studentExam = examUtilService.addStudentExamWithUser(exam, user); + ProgrammingExerciseStudentParticipation participation = (ProgrammingExerciseStudentParticipation) participationUtilService + .addProgrammingParticipationWithResultForExercise(examExercise, TEST_PREFIX + "student1").getParticipation(); + studentExam.setExercises(List.of(examExercise)); + studentExam.setWorkingTime(1); + studentExamRepository.saveAndFlush(studentExam); + + instanceMessageReceiveService.processRescheduleExamDuringConduction(exam.getId()); + + verify(versionControlService, timeout(TIMEOUT_MS)).setRepositoryPermissionsToReadOnly(participation.getVcsRepositoryUrl(), examExercise.getProjectKey(), + participation.getStudents()); + verify(programmingExerciseParticipationService, timeout(TIMEOUT_MS)).lockStudentRepositoryAndParticipation(examExercise, participation); + } + /** * Sets the due date and build and test after due date for the {@code programmingExercise} to NOW + the delay. * From 3de5468e06a23fa5d8e91ad1f9b63cee471e84c1 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:33:06 +0200 Subject: [PATCH 02/32] Add batching TODO --- src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java | 2 ++ 1 file changed, 2 insertions(+) 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 c335175cf993..c884f1f208a1 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 @@ -334,7 +334,9 @@ private void updateStudentExamsAndRescheduleExercises(Exam exam, Integer working 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); From eb6e411ac350b74c074c2e1a475ef4f5338c5546 Mon Sep 17 00:00:00 2001 From: Andreas Resch Date: Thu, 28 Sep 2023 19:34:15 +0200 Subject: [PATCH 03/32] add confirmation in exam edit form if dates change --- .../manage/exams/exam-update.component.html | 10 +++++- .../manage/exams/exam-update.component.ts | 33 ++++++++++++++++++- src/main/webapp/i18n/de/exam.json | 4 +++ src/main/webapp/i18n/en/exam.json | 4 +++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.html b/src/main/webapp/app/exam/manage/exams/exam-update.component.html index fb31d85cb749..500e42e050f9 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.html @@ -1,6 +1,6 @@
-
+

{{ 'artemisApp.examManagement.createExam' | artemisTranslate }}

{{ 'artemisApp.examManagement.importExam' | artemisTranslate }}

@@ -56,6 +56,14 @@
Exam Conduction
+
+
+ +
+ Changing the times of an active exam will disrupt the workflow of the students! +
+
+
{ + this.save(); + }); + } else { + this.save(); + } + } + + private save() { this.isSaving = true; if (this.isImport) { @@ -277,6 +301,13 @@ export class ExamUpdateComponent implements OnInit { } } + get isOngoingExam(): boolean { + if (this.exam.id === undefined || this.exam.startDate === undefined || this.exam.endDate === undefined) { + return false; + } + return this.exam.startDate.isBefore(dayjs()) && this.exam.endDate.isAfter(dayjs()); + } + get isValidPublishResultsDate(): boolean { // allow instructors to set publishResultsDate later if (!this.exam.publishResultsDate) { diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index 8106ac861f66..03480ab25127 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -690,6 +690,10 @@ "actions": "Aktionen", "viewResultsOrRunDetection": "Ergebnisse anzeigen oder Plagiatsprüfung starten", "viewCases": "Fälle anzeigen" + }, + "dateChange": { + "title": "Die Änderung der Zeiten einer aktiven Klausur wird den Arbeitsablauf der Studierenden stören!", + "warning": "Das Ändern dieser Werte wird eine Benachrichtigung an alle Studierenden senden und ihren Arbeitsablauf während der Klausur stören." } }, "studentExamDetail": { diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index 5af043476e7c..a955058a5c56 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -691,6 +691,10 @@ "actions": "Actions", "viewResultsOrRunDetection": "View Results or Run Detection", "viewCases": "View Cases" + }, + "dateChange": { + "title": "Changing the times of an active exam will disrupt the workflow of the students!", + "warning": "Updating this field will send out a notification to all students and disrupt their workflow during the exam." } }, "studentExamDetail": { From 131af6998af618c8bd45f4d8063d33263c0542bc Mon Sep 17 00:00:00 2001 From: Andreas Resch Date: Thu, 28 Sep 2023 19:47:25 +0200 Subject: [PATCH 04/32] fix if condition --- src/main/webapp/app/exam/manage/exams/exam-update.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts index 403884e745a0..778d84a5c677 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts @@ -115,7 +115,7 @@ export class ExamUpdateComponent implements OnInit { */ handleSave() { const datesChanged = this.exam.startDate?.diff(this.originalStartDate) !== 0 || this.exam.endDate?.diff(this.originalEndDate) !== 0; - if (datesChanged && this.exam.id !== undefined) { + if (datesChanged && this.isOngoingExam) { const modalRef = this.modalService.open(ConfirmAutofocusModalComponent, { keyboard: true, size: 'lg' }); modalRef.componentInstance.title = 'artemisApp.examManagement.dateChange.title'; modalRef.componentInstance.text = this.artemisTranslatePipe.transform('artemisApp.examManagement.dateChange.warning'); From 6ac8ff4656ee873a9a6caea0591d50fd167bc16f Mon Sep 17 00:00:00 2001 From: Andreas Resch Date: Thu, 28 Sep 2023 19:51:12 +0200 Subject: [PATCH 05/32] add missing lines --- src/main/webapp/app/exam/manage/exams/exam-update.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts index 778d84a5c677..0e28c56769ea 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts @@ -68,6 +68,8 @@ export class ExamUpdateComponent implements OnInit { ngOnInit(): void { this.route.data.subscribe(({ exam }) => { this.exam = exam; + this.originalStartDate = this.exam.startDate?.clone(); + this.originalEndDate = this.exam.endDate?.clone(); // Tap the URL to determine, if the Exam should be imported this.route.url.pipe(tap((segments) => (this.isImport = segments.some((segment) => segment.path === 'import')))).subscribe(); From 2996f72904faf1dde8d920e72660b76f82c93719 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Thu, 28 Sep 2023 23:50:00 +0200 Subject: [PATCH 06/32] Prettify code, make date inputs depend on each other, fix test errors, fix linter warnings --- .../manage/exams/exam-update.component.html | 6 +- .../manage/exams/exam-update.component.ts | 67 ++++++++++++------- .../date-time-picker.component.ts | 8 +-- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.html b/src/main/webapp/app/exam/manage/exams/exam-update.component.html index 500e42e050f9..68f3796caf6b 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.html @@ -1,6 +1,6 @@
- +

{{ 'artemisApp.examManagement.createExam' | artemisTranslate }}

{{ 'artemisApp.examManagement.importExam' | artemisTranslate }}

@@ -82,6 +82,7 @@
[(ngModel)]="exam.startDate" [error]="!isValidStartDate" (valueChange)="calculateMaxWorkingTime()" + [startAt]="exam.visibleDate" name="startDate" id="startDate" > @@ -93,6 +94,7 @@
[(ngModel)]="exam.endDate" [error]="!isValidEndDate" (valueChange)="calculateMaxWorkingTime()" + [startAt]="exam.startDate" name="endDate" id="endDate" > @@ -247,7 +249,7 @@
Exam Tex
{{ 'artemisApp.examManagement.reviewDatesInvalidExplanation' | artemisTranslate }}
- +
diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts index 0e28c56769ea..f83203ef25cb 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Exam } from 'app/entities/exam.model'; import { ExamManagementService } from 'app/exam/manage/exam-management.service'; -import { Observable } from 'rxjs'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; import { Course, isMessagingEnabled } from 'app/entities/course.model'; @@ -11,7 +10,7 @@ import dayjs from 'dayjs/esm'; import { onError } from 'app/shared/util/global.utils'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faBan, faCheckDouble, faExclamationTriangle, faFont, faSave } from '@fortawesome/free-solid-svg-icons'; -import { tap } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { ExerciseType } from 'app/entities/exercise.model'; import { ExamExerciseImportComponent } from 'app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; @@ -113,56 +112,72 @@ export class ExamUpdateComponent implements OnInit { } /** - * Change the date of the exam. Ask for confirmation. + * Saves the exam. If the dates have changed and the exam is ongoing, a confirmation modal is shown to the user. + * If either the user confirms the modal, the exam is not ongoing or the dates have not changed, the exam is saved. */ - handleSave() { + handleSubmit() { const datesChanged = this.exam.startDate?.diff(this.originalStartDate) !== 0 || this.exam.endDate?.diff(this.originalEndDate) !== 0; if (datesChanged && this.isOngoingExam) { const modalRef = this.modalService.open(ConfirmAutofocusModalComponent, { keyboard: true, size: 'lg' }); modalRef.componentInstance.title = 'artemisApp.examManagement.dateChange.title'; modalRef.componentInstance.text = this.artemisTranslatePipe.transform('artemisApp.examManagement.dateChange.warning'); - modalRef.result.then(() => { - this.save(); - }); + modalRef.result.then(this.save.bind(this)); } else { this.save(); } } - private save() { + /** + * Saves the exam and navigates to the detail page of the exam if the save was successful. + * If the save was not successful, an error is shown to the user. + */ + save() { this.isSaving = true; - if (this.isImport) { + this.upsertOrImportExam() + ?.pipe(map((response: HttpResponse) => response.body!)) + .subscribe({ + next: this.onSaveSuccess.bind(this), + error: this.onSaveError.bind(this), + }); + } + + /** + * Creates, updates or imports the exam depending on the current state of the component. + * @private + */ + private upsertOrImportExam() { + if (this.isImport && this.exam?.exerciseGroups) { // We validate the user input for the exercise group selection here, so it is only called once the user desires to import the exam - if (this.exam?.exerciseGroups) { - if (!this.examExerciseImportComponent.validateUserInput()) { - this.alertService.error('artemisApp.examManagement.exerciseGroup.importModal.invalidExerciseConfiguration'); - this.isSaving = false; - return; - } - this.exam.exerciseGroups = this.examExerciseImportComponent.mapSelectedExercisesToExerciseGroups(); + if (!this.examExerciseImportComponent.validateUserInput()) { + this.alertService.error('artemisApp.examManagement.exerciseGroup.importModal.invalidExerciseConfiguration'); + this.isSaving = false; + return; } - this.subscribeToSaveResponse(this.examManagementService.import(this.course.id!, this.exam)); + return this.examManagementService.import(this.course.id!, this.exam); } else if (this.exam.id !== undefined) { - this.subscribeToSaveResponse(this.examManagementService.update(this.course.id!, this.exam)); + return this.examManagementService.update(this.course.id!, this.exam); } else { - this.subscribeToSaveResponse(this.examManagementService.create(this.course.id!, this.exam)); + return this.examManagementService.create(this.course.id!, this.exam); } } - subscribeToSaveResponse(result: Observable>) { - result.subscribe({ - next: (response: HttpResponse) => this.onSaveSuccess(response.body!), - error: (err: HttpErrorResponse) => this.onSaveError(err), - }); - } - + /** + * Navigates to the detail page of the exam if the save was successful. + * @param exam + * @private + */ private onSaveSuccess(exam: Exam) { this.isSaving = false; this.router.navigate(['course-management', this.course.id, 'exams', exam.id]); window.scrollTo(0, 0); } + /** + * Shows an error to the user if the save was not successful. + * @param httpErrorResponse + * @private + */ private onSaveError(httpErrorResponse: HttpErrorResponse) { const errorKey = httpErrorResponse.error?.errorKey; if (errorKey === 'invalidKey') { diff --git a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts index b143f95ab9dd..4f1236fcf791 100644 --- a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts +++ b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts @@ -22,7 +22,7 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { @Input() value: any; @Input() disabled: boolean; @Input() error: boolean; - @Input() startAt: dayjs.Dayjs = dayjs().startOf('minutes'); // Default selected date. By default this sets it to the current time without seconds or milliseconds; + @Input() startAt?: dayjs.Dayjs = dayjs().startOf('minutes'); // Default selected date. By default this sets it to the current time without seconds or milliseconds; @Input() min: dayjs.Dayjs; // Dates before this date are not selectable. @Input() max: dayjs.Dayjs; // Dates after this date are not selectable. @Input() shouldDisplayTimeZoneWarning = true; // Displays a warning that the current time zone might differ from the participants'. @@ -35,7 +35,7 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { faQuestionCircle = faQuestionCircle; // eslint-disable-next-line @typescript-eslint/no-unused-vars - _onChange = (val: dayjs.Dayjs) => {}; + private onChange?: (val?: dayjs.Dayjs) => void; /** * Emits the value change from component. @@ -78,7 +78,7 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { * @param fn */ registerOnChange(fn: any) { - this._onChange = fn; + this.onChange = fn; } /** @@ -87,7 +87,7 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { */ updateField(newValue: dayjs.Dayjs) { this.value = newValue; - this._onChange(dayjs(this.value)); + this.onChange?.(dayjs(this.value)); this.valueChanged(); } From 6538c4f20805009e6305015d4bb1940e7ec682ac Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 29 Sep 2023 00:11:55 +0200 Subject: [PATCH 07/32] Fix client tests --- .../date-time-picker/date-time-picker.component.ts | 1 - .../shared/date-time-picker.component.spec.ts | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts index 4f1236fcf791..67eed19e0e67 100644 --- a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts +++ b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts @@ -34,7 +34,6 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { faClock = faClock; faQuestionCircle = faQuestionCircle; - // eslint-disable-next-line @typescript-eslint/no-unused-vars private onChange?: (val?: dayjs.Dayjs) => void; /** diff --git a/src/test/javascript/spec/component/shared/date-time-picker.component.spec.ts b/src/test/javascript/spec/component/shared/date-time-picker.component.spec.ts index e2220d7505a5..313338db76c5 100644 --- a/src/test/javascript/spec/component/shared/date-time-picker.component.spec.ts +++ b/src/test/javascript/spec/component/shared/date-time-picker.component.spec.ts @@ -73,16 +73,19 @@ describe('FormDateTimePickerComponent', () => { }); it('should register callback function', () => { - const testCallBackFunction = (date: dayjs.Dayjs) => 'I am a test callbackFunction: ' + date.toDate(); + const onChangeSpy = jest.fn(); + component.registerOnChange(onChangeSpy); - component.registerOnChange(testCallBackFunction); + (component as any).onChange?.(normalDate); - expect(component._onChange(normalDate)).toBe(testCallBackFunction(normalDate)); + expect(onChangeSpy).toHaveBeenCalledOnce(); + expect(onChangeSpy).toHaveBeenCalledWith(normalDate); }); it('should update field', () => { + const onChangeSpy = jest.fn(); + component.registerOnChange(onChangeSpy); const valueChangedStub = jest.spyOn(component, 'valueChanged').mockImplementation(); - const onChangeSpy = jest.spyOn(component, '_onChange'); const newDate = normalDate.add(2, 'days'); component.value = normalDate; From eb05970a5832ff3456850cdca4937f99d76203bb Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 29 Sep 2023 01:20:16 +0200 Subject: [PATCH 08/32] Code cleanup & working time calc refactoring --- .../manage/exams/exam-update.component.html | 7 +- .../manage/exams/exam-update.component.ts | 126 ++++++++++-------- .../exam/exam-update.component.spec.ts | 19 +-- 3 files changed, 81 insertions(+), 71 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.html b/src/main/webapp/app/exam/manage/exams/exam-update.component.html index 68f3796caf6b..648b2e79bbb4 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.html @@ -80,8 +80,8 @@
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.startDate' | artemisTranslate }}" labelTooltip="{{ 'artemisApp.examManagement.startDateTooltip' | artemisTranslate }}" [(ngModel)]="exam.startDate" + (valueChange)="handleExamDateChange()" [error]="!isValidStartDate" - (valueChange)="calculateMaxWorkingTime()" [startAt]="exam.visibleDate" name="startDate" id="startDate" @@ -92,8 +92,8 @@
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.endDate' | artemisTranslate }}" labelTooltip="{{ 'artemisApp.examManagement.endDateTooltip' | artemisTranslate }}" [(ngModel)]="exam.endDate" + (valueChange)="handleExamDateChange()" [error]="!isValidEndDate" - (valueChange)="calculateMaxWorkingTime()" [startAt]="exam.startDate" name="endDate" id="endDate" @@ -104,7 +104,7 @@
- + [customMin]="1" [customMax]="maxWorkingTimeInMinutes" [(ngModel)]="workingTimeInMinutes" - (change)="convertWorkingTimeFromMinutesToSeconds($event)" />
diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts index f83203ef25cb..a7b14bf0642b 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts @@ -30,7 +30,7 @@ export class ExamUpdateComponent implements OnInit { course: Course; isSaving: boolean; // The exam.workingTime is stored in seconds, but the working time should be displayed in minutes to the user - workingTimeInMinutes: number; + // workingTimeInMinutes: number; // The maximum working time in Minutes (used as a dynamic max-value for the working time Input) maxWorkingTimeInMinutes: number; isImport = false; @@ -82,7 +82,7 @@ export class ExamUpdateComponent implements OnInit { next: (response: HttpResponse) => { this.exam.course = response.body!; this.course = response.body!; - this.hideChannelNameInput = (exam.id !== undefined && exam.channelName === undefined) || !isMessagingEnabled(this.course); + this.hideChannelNameInput = (!!exam.id && !exam.channelName) || !isMessagingEnabled(this.course); }, error: (err: HttpErrorResponse) => onError(this.alertService, err), }); @@ -98,10 +98,24 @@ export class ExamUpdateComponent implements OnInit { } }); // Initialize helper attributes - this.workingTimeInMinutes = this.exam.workingTime! / 60; this.calculateMaxWorkingTime(); } + /** + * Sets the exam working time in minutes. + * @param minutes + */ + set workingTimeInMinutes(minutes: number) { + this.exam.workingTime = minutes * 60; + } + + /** + * Returns the exam working time in minutes. + */ + get workingTimeInMinutes(): number { + return this.exam.workingTime ? this.exam.workingTime / 60 : 0; + } + /** * Revert to the previous state, equivalent with pressing the back button on your browser * Returns to the detail page if there is no previous state and we edited an existing exam @@ -111,6 +125,40 @@ export class ExamUpdateComponent implements OnInit { this.navigationUtilService.navigateBackWithOptional(['course-management', this.course.id!.toString(), 'exams'], this.exam.id?.toString()); } + handleExamDateChange() { + console.log('ALARM'); + this.calculateWorkingTime(); + this.calculateMaxWorkingTime(); + } + + /** + * Calculates the WorkingTime for real exams based on the start- and end-time. + */ + private calculateWorkingTime() { + if (!this.exam.testExam) { + if (this.exam.startDate && this.exam.endDate) { + this.exam.workingTime = dayjs(this.exam.endDate).diff(this.exam.startDate, 's'); + } else { + this.exam.workingTime = 0; + } + } + } + + /** + * Used to determine the maximum working time every time, the user changes the start- or endDate. + * Used to show a graphical warning at the working time input field + */ + private calculateMaxWorkingTime() { + if (this.exam.testExam) { + if (this.exam.startDate && this.exam.endDate) { + this.maxWorkingTimeInMinutes = dayjs(this.exam.endDate).diff(this.exam.startDate, 's') / 60; + } else { + // In case of an import, the exam.workingTime is imported, but the start / end date are deleted -> no error should be shown to the user in this case + this.maxWorkingTimeInMinutes = this.isImport ? this.workingTimeInMinutes : 0; + } + } + } + /** * Saves the exam. If the dates have changed and the exam is ongoing, a confirmation modal is shown to the user. * If either the user confirms the modal, the exam is not ongoing or the dates have not changed, the exam is saved. @@ -155,7 +203,7 @@ export class ExamUpdateComponent implements OnInit { return; } return this.examManagementService.import(this.course.id!, this.exam); - } else if (this.exam.id !== undefined) { + } else if (this.exam.id) { return this.examManagementService.update(this.course.id!, this.exam); } else { return this.examManagementService.create(this.course.id!, this.exam); @@ -200,6 +248,13 @@ export class ExamUpdateComponent implements OnInit { this.isSaving = false; } + /** + * Returns true if the exam is currently ongoing, false otherwise. + */ + get isOngoingExam(): boolean { + return !!this.exam.id && !!this.exam.startDate && !!this.exam.endDate && dayjs().isBetween(this.exam.startDate, this.exam.endDate); + } + get isValidConfiguration(): boolean { const examConductionDatesValid = this.isValidVisibleDate && this.isValidStartDate && this.isValidEndDate; const examReviewDatesValid = this.isValidPublishResultsDate && this.isValidExamStudentReviewStart && this.isValidExamStudentReviewEnd; @@ -218,7 +273,7 @@ export class ExamUpdateComponent implements OnInit { } get isValidVisibleDate(): boolean { - return this.exam.visibleDate !== undefined; + return !!this.exam.visibleDate; } get isValidNumberOfCorrectionRounds(): boolean { @@ -231,7 +286,7 @@ export class ExamUpdateComponent implements OnInit { } get isValidMaxPoints(): boolean { - return this.exam?.examMaxPoints !== undefined && this.exam?.examMaxPoints > 0; + return !!this.exam?.examMaxPoints && this.exam?.examMaxPoints > 0; } /** @@ -240,7 +295,7 @@ export class ExamUpdateComponent implements OnInit { * For test exams, the visibleDate has to be prior or equal to the startDate. */ get isValidStartDate(): boolean { - if (this.exam.startDate === undefined) { + if (!this.exam.startDate) { return false; } if (this.exam.testExam) { @@ -254,22 +309,7 @@ export class ExamUpdateComponent implements OnInit { * Validates the EndDate inputted by the user. */ get isValidEndDate(): boolean { - return this.exam.endDate !== undefined && dayjs(this.exam.endDate).isAfter(this.exam.startDate); - } - - /** - * Calculates the WorkingTime for real exams based on the start- and end-time. - */ - get calculateWorkingTime(): number { - if (!this.exam.testExam) { - if (this.exam.startDate && this.exam.endDate) { - this.exam.workingTime = dayjs(this.exam.endDate).diff(this.exam.startDate, 's'); - } else { - this.exam.workingTime = 0; - } - this.workingTimeInMinutes = this.exam.workingTime / 60; - } - return this.workingTimeInMinutes; + return !!this.exam.endDate && dayjs(this.exam.endDate).isAfter(this.exam.startDate); } /** @@ -279,7 +319,7 @@ export class ExamUpdateComponent implements OnInit { */ get validateWorkingTime(): boolean { if (this.exam.testExam) { - if (this.exam.workingTime === undefined || this.exam.workingTime < 1) { + if (!this.exam.workingTime || this.exam.workingTime < 1) { return false; } if (this.exam.startDate && this.exam.endDate) { @@ -293,45 +333,13 @@ export class ExamUpdateComponent implements OnInit { return false; } - /** - * Used to convert workingTimeInMinutes into exam.workingTime (in seconds) every time, the user inputs a new - * working time for a test exam - * @param event when the user inputs a new working time - */ - convertWorkingTimeFromMinutesToSeconds(event: any) { - this.workingTimeInMinutes = event.target.value; - this.exam.workingTime = this.workingTimeInMinutes * 60; - } - - /** - * Used to determine the maximum working time every time, the user changes the start- or endDate. - * Used to show a graphical warning at the working time input field - */ - calculateMaxWorkingTime() { - if (this.exam.testExam) { - if (this.exam.startDate && this.exam.endDate) { - this.maxWorkingTimeInMinutes = dayjs(this.exam.endDate).diff(this.exam.startDate, 's') / 60; - } else { - // In case of an import, the exam.workingTime is imported, but the start / end date are deleted -> no error should be shown to the user in this case - this.maxWorkingTimeInMinutes = this.isImport ? this.workingTimeInMinutes : 0; - } - } - } - - get isOngoingExam(): boolean { - if (this.exam.id === undefined || this.exam.startDate === undefined || this.exam.endDate === undefined) { - return false; - } - return this.exam.startDate.isBefore(dayjs()) && this.exam.endDate.isAfter(dayjs()); - } - get isValidPublishResultsDate(): boolean { // allow instructors to set publishResultsDate later if (!this.exam.publishResultsDate) { return true; } // check for undefined because undefined is otherwise treated as the now dayjs - return this.exam.endDate !== undefined && dayjs(this.exam.publishResultsDate).isAfter(this.exam.endDate); + return !!this.exam.endDate && dayjs(this.exam.publishResultsDate).isAfter(this.exam.endDate); } get isValidExamStudentReviewStart(): boolean { @@ -340,7 +348,7 @@ export class ExamUpdateComponent implements OnInit { return true; } // check for undefined because undefined is otherwise treated as the now dayjs - return this.exam.publishResultsDate !== undefined && dayjs(this.exam.examStudentReviewStart).isAfter(this.exam.publishResultsDate); + return !!this.exam.publishResultsDate && dayjs(this.exam.examStudentReviewStart).isAfter(this.exam.publishResultsDate); } get isValidExamStudentReviewEnd(): boolean { @@ -349,7 +357,7 @@ export class ExamUpdateComponent implements OnInit { return !this.exam.examStudentReviewStart || !this.exam.examStudentReviewStart.isValid(); } // check for undefined because undefined is otherwise treated as the now dayjs - return this.exam.examStudentReviewStart !== undefined && dayjs(this.exam.examStudentReviewEnd).isAfter(this.exam.examStudentReviewStart); + return !!this.exam.examStudentReviewStart && dayjs(this.exam.examStudentReviewEnd).isAfter(this.exam.examStudentReviewStart); } get isValidExampleSolutionPublicationDate(): boolean { diff --git a/src/test/javascript/spec/component/exam/exam-update.component.spec.ts b/src/test/javascript/spec/component/exam/exam-update.component.spec.ts index a5e668ad7e97..c2a4bb1c20fd 100644 --- a/src/test/javascript/spec/component/exam/exam-update.component.spec.ts +++ b/src/test/javascript/spec/component/exam/exam-update.component.spec.ts @@ -255,39 +255,42 @@ describe('Exam Update Component', () => { })); it('should calculate the working time for real exams correctly', () => { + fixture.detectChanges(); + examWithoutExercises.testExam = false; examWithoutExercises.startDate = undefined; examWithoutExercises.endDate = dayjs().add(2, 'hours'); - fixture.detectChanges(); + component.handleExamDateChange(); // Without a valid startDate, the workingTime should be 0 // examWithoutExercises.workingTime is stored in seconds expect(examWithoutExercises.workingTime).toBe(0); // the component returns the workingTime in Minutes - expect(component.calculateWorkingTime).toBe(0); + expect(component.workingTimeInMinutes).toBe(0); examWithoutExercises.startDate = dayjs().add(0, 'hours'); examWithoutExercises.endDate = dayjs().add(2, 'hours'); - fixture.detectChanges(); + component.handleExamDateChange(); expect(examWithoutExercises.workingTime).toBe(7200); - expect(component.calculateWorkingTime).toBe(120); + expect(component.workingTimeInMinutes).toBe(120); examWithoutExercises.startDate = dayjs().add(0, 'hours'); examWithoutExercises.endDate = undefined; - fixture.detectChanges(); + component.handleExamDateChange(); // Without an endDate, the working time should be 0; expect(examWithoutExercises.workingTime).toBe(0); - expect(component.calculateWorkingTime).toBe(0); + expect(component.workingTimeInMinutes).toBe(0); }); it('should not calculate the working time for test exams', () => { + fixture.detectChanges(); examWithoutExercises.testExam = true; examWithoutExercises.workingTime = 3600; examWithoutExercises.startDate = dayjs().add(0, 'hours'); examWithoutExercises.endDate = dayjs().add(12, 'hours'); - fixture.detectChanges(); + component.handleExamDateChange(); expect(examWithoutExercises.workingTime).toBe(3600); - expect(component.calculateWorkingTime).toBe(60); + expect(component.workingTimeInMinutes).toBe(60); }); it('validates the working time for test exams correctly', () => { From 7890241eda66107b04c365f0c7848ce9908625d5 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 29 Sep 2023 01:22:37 +0200 Subject: [PATCH 09/32] Remove console log --- src/main/webapp/app/exam/manage/exams/exam-update.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts index a7b14bf0642b..d6c854d4520b 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts @@ -126,7 +126,6 @@ export class ExamUpdateComponent implements OnInit { } handleExamDateChange() { - console.log('ALARM'); this.calculateWorkingTime(); this.calculateMaxWorkingTime(); } From b8476ac3e159eb53d181fc6447ddc080cbb719f8 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 29 Sep 2023 04:14:45 +0200 Subject: [PATCH 10/32] Fix bugs in import client tests, adapt client tests to changes, major exam update component refactoring --- .../manage/exam-management-resolve.service.ts | 20 +++ .../app/exam/manage/exam-management.route.ts | 5 +- .../manage/exams/exam-update.component.html | 6 +- .../manage/exams/exam-update.component.ts | 149 +++++++----------- .../exam/exam-update.component.spec.ts | 90 ++++------- 5 files changed, 117 insertions(+), 153 deletions(-) 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..cdefedc535c2 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,26 @@ 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'; + +@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( + filter((response) => response.ok), + map((response) => response.body!), + ); + } + + return of(new Course()); + } +} @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-update.component.html b/src/main/webapp/app/exam/manage/exams/exam-update.component.html index 648b2e79bbb4..a9034a731cd1 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.html @@ -80,7 +80,7 @@
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.startDate' | artemisTranslate }}" labelTooltip="{{ 'artemisApp.examManagement.startDateTooltip' | artemisTranslate }}" [(ngModel)]="exam.startDate" - (valueChange)="handleExamDateChange()" + (valueChange)="calculateWorkingTime()" [error]="!isValidStartDate" [startAt]="exam.visibleDate" name="startDate" @@ -92,7 +92,7 @@
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.endDate' | artemisTranslate }}" labelTooltip="{{ 'artemisApp.examManagement.endDateTooltip' | artemisTranslate }}" [(ngModel)]="exam.endDate" - (valueChange)="handleExamDateChange()" + (valueChange)="calculateWorkingTime()" [error]="!isValidEndDate" [startAt]="exam.startDate" name="endDate" @@ -239,7 +239,7 @@
Exam Tex
-
[(ngModel)]="exam.startDate" (valueChange)="calculateWorkingTime()" [error]="!isValidStartDate" - [startAt]="exam.visibleDate" + [startAt]="exam.startDate || exam.visibleDate" name="startDate" id="startDate" - > + />
[(ngModel)]="exam.endDate" (valueChange)="calculateWorkingTime()" [error]="!isValidEndDate" - [startAt]="exam.startDate" + [startAt]="exam.endDate || exam.startDate" name="endDate" id="endDate" - > + />
diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts index 46142ab94aba..5d54426e229e 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts @@ -99,8 +99,8 @@ export class ExamUpdateComponent implements OnInit { /** * Revert to the previous state, equivalent with pressing the back button on your browser - * Returns to the detail page if there is no previous state and we edited an existing exam - * Returns to the overview page if there is no previous state and we created a new exam + * Returns to the detail page if there is no previous state, and we edited an existing exam + * Returns to the overview page if there is no previous state, and we created a new exam */ resetToPreviousState() { this.navigationUtilService.navigateBackWithOptional(['course-management', this.course.id!.toString(), 'exams'], this.exam.id?.toString()); diff --git a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.html b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.html index e04e178dd6e7..438f3ed1cf3d 100644 --- a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.html +++ b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.html @@ -16,8 +16,8 @@ [ngClass]="{ 'is-invalid': error }" [ngModel]="value" [disabled]="disabled" - [min]="convert(min)" - [max]="convert(max)" + [min]="minDate" + [max]="maxDate" (ngModelChange)="updateField($event)" [owlDateTime]="dt" name="datePicker" @@ -25,5 +25,5 @@ - +
diff --git a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts index 67eed19e0e67..9151067c516a 100644 --- a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts +++ b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts @@ -22,7 +22,7 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { @Input() value: any; @Input() disabled: boolean; @Input() error: boolean; - @Input() startAt?: dayjs.Dayjs = dayjs().startOf('minutes'); // Default selected date. By default this sets it to the current time without seconds or milliseconds; + @Input() startAt?: dayjs.Dayjs; // Default selected date. By default this sets it to the current time without seconds or milliseconds; @Input() min: dayjs.Dayjs; // Dates before this date are not selectable. @Input() max: dayjs.Dayjs; // Dates after this date are not selectable. @Input() shouldDisplayTimeZoneWarning = true; // Displays a warning that the current time zone might differ from the participants'. @@ -43,15 +43,6 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { this.valueChange.emit(); } - /** - * Function that converts a possibly undefined dayjs value to a date or null. - * - * @param value as dayjs - */ - convert(value?: dayjs.Dayjs) { - return value != undefined && value.isValid() ? value.toDate() : null; - } - /** * Function that writes the value safely. * @param value as dayjs or date @@ -96,4 +87,25 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { get currentTimeZone(): string { return Intl.DateTimeFormat().resolvedOptions().timeZone; } + + get startDate(): Date | null { + return this.convertToDate(this.startAt ?? dayjs().startOf('minutes')); + } + + get minDate(): Date | null { + return this.convertToDate(this.min); + } + + get maxDate(): Date | null { + return this.convertToDate(this.max); + } + + /** + * Function that converts a possibly undefined dayjs value to a date or null. + * + * @param value as dayjs + */ + private convertToDate(value?: dayjs.Dayjs) { + return value != undefined && value.isValid() ? value.toDate() : null; + } } From 15a0b5d4b7e5181383102655211c57ceeec080bb Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:07:23 +0200 Subject: [PATCH 14/32] Revert working time check --- src/main/webapp/app/exam/manage/exams/exam-update.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts index 5d54426e229e..a6280b9ba96b 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts @@ -292,7 +292,7 @@ export class ExamUpdateComponent implements OnInit { */ get validateWorkingTime(): boolean { if (this.exam.testExam) { - if (!this.exam.workingTime || this.exam.workingTime < 1) { + if (this.exam.workingTime === undefined || this.exam.workingTime < 1) { return false; } if (this.exam.startDate && this.exam.endDate) { From 1dc0d2f2287cac04f8e1eb40f41c63aed1c908f6 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:20:11 +0200 Subject: [PATCH 15/32] Fix issue w/ working time calculation --- .../tum/in/www1/artemis/web/rest/ExamResource.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 04465ae70ef2..214360b75a1a 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 @@ -202,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()) { @@ -233,7 +234,9 @@ public ResponseEntity updateExam(@PathVariable Long courseId, @RequestBody if (comparator.compare(originalExam.getEndDate(), savedExam.getEndDate()) != 0) { // TODO: check if there are any problems with that - i.e. TZ issues, only changing working time in UI, ... int workingTimeChange = savedExam.getDuration() - originalExam.getDuration(); - updateStudentExamsAndRescheduleExercises(examWithExercises, workingTimeChange); + System.out.println("Working Time Change:"); + System.out.println(workingTimeChange); + updateStudentExamsAndRescheduleExercises(examWithExercises, originalExamDuration, workingTimeChange); } if (updatedChannel != null) { @@ -264,6 +267,7 @@ public ResponseEntity updateExamWorkingTime(@PathVariable Long courseId, @ // NOTE: We have to get exercise groups as `scheduleModelingExercises` needs them Exam exam = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(examId); + var originalExamDuration = exam.getDuration(); // 1. Update the end date & working time of the exam exam.setEndDate(exam.getEndDate().plusSeconds(workingTimeChange)); @@ -271,13 +275,15 @@ public ResponseEntity updateExamWorkingTime(@PathVariable Long courseId, @ examRepository.save(exam); // 2. Re-calculate the working times of all student exams - updateStudentExamsAndRescheduleExercises(exam, workingTimeChange); + updateStudentExamsAndRescheduleExercises(exam, originalExamDuration, workingTimeChange); return ResponseEntity.ok(exam); } - private void updateStudentExamsAndRescheduleExercises(Exam exam, Integer workingTimeChange) { - var originalExamDuration = exam.getDuration(); + private void updateStudentExamsAndRescheduleExercises(Exam exam, Integer originalExamDuration, Integer workingTimeChange) { + + System.out.println("Original Exam Duration:"); + System.out.println(originalExamDuration); var now = now(); From 66780deb1657432d99556146f723bf58ea69e779 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:20:40 +0200 Subject: [PATCH 16/32] Fix issue w/ working time calculation --- .../java/de/tum/in/www1/artemis/web/rest/ExamResource.java | 6 ------ 1 file changed, 6 deletions(-) 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 214360b75a1a..1e4581404526 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 @@ -234,8 +234,6 @@ public ResponseEntity updateExam(@PathVariable Long courseId, @RequestBody if (comparator.compare(originalExam.getEndDate(), savedExam.getEndDate()) != 0) { // TODO: check if there are any problems with that - i.e. TZ issues, only changing working time in UI, ... int workingTimeChange = savedExam.getDuration() - originalExam.getDuration(); - System.out.println("Working Time Change:"); - System.out.println(workingTimeChange); updateStudentExamsAndRescheduleExercises(examWithExercises, originalExamDuration, workingTimeChange); } @@ -281,10 +279,6 @@ public ResponseEntity updateExamWorkingTime(@PathVariable Long courseId, @ } private void updateStudentExamsAndRescheduleExercises(Exam exam, Integer originalExamDuration, Integer workingTimeChange) { - - System.out.println("Original Exam Duration:"); - System.out.println(originalExamDuration); - var now = now(); User instructor = userRepository.getUser(); From 02f7ac64b76ec5cbee907c5a7de9d5d5aca142ce Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:23:05 +0200 Subject: [PATCH 17/32] Remove TODO --- src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java | 1 - 1 file changed, 1 deletion(-) 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 1e4581404526..7b5fa8dd98dc 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 @@ -232,7 +232,6 @@ public ResponseEntity updateExam(@PathVariable Long courseId, @RequestBody // NOTE: if the end date was changed, we need to update student exams and re-schedule exercises if (comparator.compare(originalExam.getEndDate(), savedExam.getEndDate()) != 0) { - // TODO: check if there are any problems with that - i.e. TZ issues, only changing working time in UI, ... int workingTimeChange = savedExam.getDuration() - originalExam.getDuration(); updateStudentExamsAndRescheduleExercises(examWithExercises, originalExamDuration, workingTimeChange); } From 0bc7fabfdadfacee3fb1bb16f156ebf9fefba09a Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:29:56 +0200 Subject: [PATCH 18/32] Fix date time picker tests --- .../shared/date-time-picker/date-time-picker.component.ts | 2 +- .../component/shared/date-time-picker.component.spec.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts index 9151067c516a..67e7edd553cb 100644 --- a/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts +++ b/src/main/webapp/app/shared/date-time-picker/date-time-picker.component.ts @@ -105,7 +105,7 @@ export class FormDateTimePickerComponent implements ControlValueAccessor { * * @param value as dayjs */ - private convertToDate(value?: dayjs.Dayjs) { + convertToDate(value?: dayjs.Dayjs) { return value != undefined && value.isValid() ? value.toDate() : null; } } diff --git a/src/test/javascript/spec/component/shared/date-time-picker.component.spec.ts b/src/test/javascript/spec/component/shared/date-time-picker.component.spec.ts index 313338db76c5..46d0a9fc5a91 100644 --- a/src/test/javascript/spec/component/shared/date-time-picker.component.spec.ts +++ b/src/test/javascript/spec/component/shared/date-time-picker.component.spec.ts @@ -36,13 +36,13 @@ describe('FormDateTimePickerComponent', () => { describe('test date conversion', () => { let convertedDate: Date | null; it('should convert the dayjs if it is not undefined', () => { - convertedDate = component.convert(normalDate); + convertedDate = component.convertToDate(normalDate); expect(convertedDate).toEqual(normalDateAsDateObject); }); it('should return null if dayjs is undefined', () => { - convertedDate = component.convert(); + convertedDate = component.convertToDate(); expect(convertedDate).toBeNull(); }); @@ -52,7 +52,7 @@ describe('FormDateTimePickerComponent', () => { expect(unconvertedDate.isValid()).toBeFalse(); - convertedDate = component.convert(unconvertedDate); + convertedDate = component.convertToDate(unconvertedDate); expect(convertedDate).toBeNull(); }); From f6c4d2b8a3a6161290b775e2d4809f6035ab8d93 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Sun, 1 Oct 2023 17:31:28 +0200 Subject: [PATCH 19/32] Apply suggestions from code review Co-authored-by: Lucas Welscher --- .../java/de/tum/in/www1/artemis/web/rest/ExamResource.java | 2 +- src/main/webapp/i18n/en/exam.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 7b5fa8dd98dc..9886651075d1 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 @@ -232,7 +232,7 @@ public ResponseEntity updateExam(@PathVariable Long courseId, @RequestBody // NOTE: if the end date was changed, we need to update student exams and re-schedule exercises if (comparator.compare(originalExam.getEndDate(), savedExam.getEndDate()) != 0) { - int workingTimeChange = savedExam.getDuration() - originalExam.getDuration(); + int workingTimeChange = savedExam.getDuration() - originalExamDuration; updateStudentExamsAndRescheduleExercises(examWithExercises, originalExamDuration, workingTimeChange); } diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index 978d803a0cb2..9f8fd7248fc0 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -693,8 +693,8 @@ "viewCases": "View Cases" }, "dateChange": { - "title": "Changing the times of an active exam will disrupt the workflow of the students!", - "warning": "Updating this field will send out a notification to all students and disrupt their workflow during the exam." + "title": "Changing the working time of an active exam will disrupt the workflow of the students!", + "warning": "Updating this field will notify all students and disrupt their workflow during the exam." } }, "studentExamDetail": { From 91482b52fa34e6901a2968f16fe1b08978fec82d Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Fri, 6 Oct 2023 13:08:40 +0200 Subject: [PATCH 20/32] Show working time change in dialog, extract working time change component, allow negative working time change --- ...am-edit-working-time-dialog.component.html | 10 ++++---- ...exam-edit-working-time-dialog.component.ts | 12 ++++++---- .../exam-edit-working-time.component.ts | 2 +- .../events/exam-live-event.component.html | 17 ++++--------- .../events/exam-live-event.component.scss | 24 +------------------ .../events/exam-live-event.component.ts | 4 ++-- .../app/exam/shared/exam-shared.module.ts | 7 +++--- .../working-time-change.component.html | 10 ++++++++ .../working-time-change.component.ts | 10 ++++++++ .../working-time-control.component.html | 8 +++---- .../working-time-control.component.scss | 0 .../working-time-control.component.ts | 2 +- src/main/webapp/i18n/de/exam.json | 2 +- src/main/webapp/i18n/en/exam.json | 2 +- 14 files changed, 50 insertions(+), 60 deletions(-) create mode 100644 src/main/webapp/app/exam/shared/working-time-change/working-time-change.component.html create mode 100644 src/main/webapp/app/exam/shared/working-time-change/working-time-change.component.ts rename src/main/webapp/app/exam/shared/{working-time-update => working-time-control}/working-time-control.component.html (92%) rename src/main/webapp/app/exam/shared/{working-time-update => working-time-control}/working-time-control.component.scss (100%) rename src/main/webapp/app/exam/shared/{working-time-update => working-time-control}/working-time-control.component.ts (98%) 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 @@
-

+
+

diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component.ts b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component.ts index c0f8a231564d..fc64d4672b76 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component.ts @@ -1,12 +1,11 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; import { HttpResponse } from '@angular/common/http'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { faBan, faCheck, faSpinner } from '@fortawesome/free-solid-svg-icons'; import { Exam } from 'app/entities/exam.model'; import { ExamManagementService } from 'app/exam/manage/exam-management.service'; import { normalWorkingTime } from 'app/exam/participate/exam.utils'; -import dayjs from 'dayjs/esm'; @Component({ selector: 'jhi-edit-working-time-dialog', @@ -27,11 +26,14 @@ export class ExamEditWorkingTimeDialogComponent { workingTimeSeconds = 0; - get absoluteWorkingTimeDuration() { + get oldWorkingTime() { + return normalWorkingTime(this.exam); + } + + get newWorkingTime() { const currentWorkingTimeSeconds = normalWorkingTime(this.exam); if (!currentWorkingTimeSeconds) return undefined; - const duration = dayjs.duration(currentWorkingTimeSeconds + this.workingTimeSeconds, 'seconds'); - return [duration.asHours() >= 1 ? `${Math.floor(duration.asHours())} h` : null, duration.format('m [min] s [s]')].filter(Boolean).join(' '); + return currentWorkingTimeSeconds + this.workingTimeSeconds; } constructor( diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.ts b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.ts index ba39fe36298d..c64acade7caf 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.ts @@ -5,8 +5,8 @@ import dayjs from 'dayjs/esm'; import { Subscription, from } from 'rxjs'; import { Exam } from 'app/entities/exam.model'; -import { ExamEditWorkingTimeDialogComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component'; import { AlertService } from 'app/core/util/alert.service'; +import { ExamEditWorkingTimeDialogComponent } from './exam-edit-working-time-dialog.component'; @Component({ selector: 'jhi-exam-edit-working-time', diff --git a/src/main/webapp/app/exam/shared/events/exam-live-event.component.html b/src/main/webapp/app/exam/shared/events/exam-live-event.component.html index bc16073f1426..b8e3844b6db3 100644 --- a/src/main/webapp/app/exam/shared/events/exam-live-event.component.html +++ b/src/main/webapp/app/exam/shared/events/exam-live-event.component.html @@ -13,24 +13,15 @@ | {{ event.acknowledgeTimestamps!.user! | artemisDate: 'time' }}
-
+
-
+
The working time of the exam has been changed.
-
+
The working time of your exam has been changed.
-
- -
-
New working time
-
{{ eventAsWorkingTimeUpdateEvent().newWorkingTime | artemisDurationFromSeconds }}
-
-
+
+ +
+ +
+
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.startDate' | artemisTranslate }}" labelTooltip="{{ 'artemisApp.examManagement.startDateTooltip' | artemisTranslate }}" [(ngModel)]="exam.startDate" - (valueChange)="calculateWorkingTime()" + (valueChange)="updateExamWorkingTime()" [error]="!isValidStartDate" [startAt]="exam.startDate || exam.visibleDate" name="startDate" @@ -93,7 +98,7 @@
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.endDate' | artemisTranslate }}" labelTooltip="{{ 'artemisApp.examManagement.endDateTooltip' | artemisTranslate }}" [(ngModel)]="exam.endDate" - (valueChange)="calculateWorkingTime()" + (valueChange)="updateExamWorkingTime()" [error]="!isValidEndDate" [startAt]="exam.endDate || exam.startDate" name="endDate" diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts index a6280b9ba96b..70cae72a71d8 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts @@ -2,7 +2,7 @@ import dayjs from 'dayjs/esm'; import { omit } from 'lodash-es'; import { combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { faBan, faExclamationTriangle, faSave } from '@fortawesome/free-solid-svg-icons'; @@ -16,8 +16,9 @@ import { onError } from 'app/shared/util/global.utils'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { ExamExerciseImportComponent } from 'app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; -import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-button.component'; +import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-modal.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { normalWorkingTime } from 'app/exam/participate/exam.utils'; @Component({ selector: 'jhi-exam-update', @@ -36,6 +37,7 @@ export class ExamUpdateComponent implements OnInit { // Link to the component enabling the selection of exercise groups and exercises for import @ViewChild(ExamExerciseImportComponent) examExerciseImportComponent: ExamExerciseImportComponent; + @ViewChild('workingTimeConfirmationContent') public workingTimeConfirmationContent: TemplateRef; documentationType = DocumentationType.Exams; @@ -97,6 +99,14 @@ export class ExamUpdateComponent implements OnInit { return this.exam.workingTime ? this.exam.workingTime / 60 : 0; } + get oldWorkingTime(): number | undefined { + return normalWorkingTime({ startDate: this.originalStartDate, endDate: this.originalEndDate } as Exam); + } + + get newWorkingTime(): number | undefined { + return this.exam.workingTime; + } + /** * Revert to the previous state, equivalent with pressing the back button on your browser * Returns to the detail page if there is no previous state, and we edited an existing exam @@ -107,16 +117,12 @@ export class ExamUpdateComponent implements OnInit { } /** - * Calculates the WorkingTime for real exams based on the start- and end-time. + * Updates the working time for real exams based on the start and end dates. */ - calculateWorkingTime() { + updateExamWorkingTime() { if (this.exam.testExam) return; - if (this.exam.startDate && this.exam.endDate) { - this.exam.workingTime = dayjs(this.exam.endDate).diff(this.exam.startDate, 's'); - } else { - this.exam.workingTime = 0; - } + this.exam.workingTime = normalWorkingTime(this.exam); } /** @@ -142,7 +148,8 @@ export class ExamUpdateComponent implements OnInit { if (datesChanged && this.isOngoingExam) { const modalRef = this.modalService.open(ConfirmAutofocusModalComponent, { keyboard: true, size: 'lg' }); modalRef.componentInstance.title = 'artemisApp.examManagement.dateChange.title'; - modalRef.componentInstance.text = this.artemisTranslatePipe.transform('artemisApp.examManagement.dateChange.warning'); + modalRef.componentInstance.text = this.artemisTranslatePipe.transform('artemisApp.examManagement.dateChange.message'); + modalRef.componentInstance.contentRef = this.workingTimeConfirmationContent; modalRef.result.then(this.save.bind(this)); } else { this.save(); @@ -223,10 +230,10 @@ export class ExamUpdateComponent implements OnInit { } /** - * Returns true if the exam is currently ongoing, false otherwise. + * Returns true if the original exam is currently ongoing before any changes, false otherwise. */ get isOngoingExam(): boolean { - return !!this.exam.id && !!this.exam.startDate && !!this.exam.endDate && dayjs().isBetween(this.exam.startDate, this.exam.endDate); + return !!this.exam.id && !!this.originalStartDate && !!this.originalEndDate && dayjs().isBetween(this.originalStartDate, this.originalEndDate); } get isValidConfiguration(): boolean { diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exams.component.ts b/src/main/webapp/app/exam/manage/student-exams/student-exams.component.ts index 53123eb952bb..8ca3aed74f49 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exams.component.ts +++ b/src/main/webapp/app/exam/manage/student-exams/student-exams.component.ts @@ -11,7 +11,7 @@ import { ExamManagementService } from 'app/exam/manage/exam-management.service'; import { AlertService } from 'app/core/util/alert.service'; import { HttpErrorResponse } from '@angular/common/http'; import { Exam } from 'app/entities/exam.model'; -import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-button.component'; +import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-modal.component'; import dayjs from 'dayjs/esm'; import { AccountService } from 'app/core/auth/account.service'; import { onError } from 'app/shared/util/global.utils'; diff --git a/src/main/webapp/app/exercises/programming/hestia/generation-overview/steps/solution-entry-generation-step/solution-entry-generation-step.component.ts b/src/main/webapp/app/exercises/programming/hestia/generation-overview/steps/solution-entry-generation-step/solution-entry-generation-step.component.ts index e06fceb30460..13a0b57ec028 100644 --- a/src/main/webapp/app/exercises/programming/hestia/generation-overview/steps/solution-entry-generation-step/solution-entry-generation-step.component.ts +++ b/src/main/webapp/app/exercises/programming/hestia/generation-overview/steps/solution-entry-generation-step/solution-entry-generation-step.component.ts @@ -12,7 +12,7 @@ import { CodeHintService } from 'app/exercises/shared/exercise-hint/services/cod import { ManualSolutionEntryCreationModalComponent } from 'app/exercises/programming/hestia/generation-overview/manual-solution-entry-creation-modal/manual-solution-entry-creation-modal.component'; import { SortingOrder } from 'app/shared/table/pageable-table'; import { ProgrammingExerciseSolutionEntryService } from 'app/exercises/shared/exercise-hint/services/programming-exercise-solution-entry.service'; -import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-button.component'; +import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-modal.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; @Component({ @@ -70,14 +70,20 @@ export class SolutionEntryGenerationStepComponent implements OnInit, OnDestroy { } openSolutionEntryModal(solutionEntry: ProgrammingExerciseSolutionEntry, isEditable: boolean) { - const modalRef: NgbModalRef = this.modalService.open(SolutionEntryDetailsModalComponent as Component, { size: 'lg', backdrop: 'static' }); + const modalRef: NgbModalRef = this.modalService.open(SolutionEntryDetailsModalComponent as Component, { + size: 'lg', + backdrop: 'static', + }); modalRef.componentInstance.exerciseId = this.exercise.id; modalRef.componentInstance.solutionEntry = solutionEntry; modalRef.componentInstance.isEditable = isEditable; } openManualEntryCreationModal() { - const modalRef: NgbModalRef = this.modalService.open(ManualSolutionEntryCreationModalComponent as Component, { size: 'lg', backdrop: 'static' }); + const modalRef: NgbModalRef = this.modalService.open(ManualSolutionEntryCreationModalComponent as Component, { + size: 'lg', + backdrop: 'static', + }); modalRef.componentInstance.exerciseId = this.exercise.id; modalRef.componentInstance.onEntryCreated.subscribe((createdEntry: ProgrammingExerciseSolutionEntry) => { this.solutionEntries.push(createdEntry); diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts index a8a577224673..189f8d20fc30 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts @@ -13,7 +13,7 @@ import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service' import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { ExerciseType } from 'app/entities/exercise.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-button.component'; +import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-modal.component'; import { TranslateService } from '@ngx-translate/core'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { ExerciseManagementStatisticsDto } from 'app/exercises/shared/statistics/exercise-management-statistics-dto'; diff --git a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-trigger-build-button.component.ts b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-trigger-build-button.component.ts index dbb8693adec5..91ced7a711aa 100644 --- a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-trigger-build-button.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-trigger-build-button.component.ts @@ -6,7 +6,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ParticipationWebsocketService } from 'app/overview/participation-websocket.service'; import { AlertService } from 'app/core/util/alert.service'; import { SubmissionType } from 'app/entities/submission.model'; -import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-button.component'; +import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-modal.component'; import { faRedo } from '@fortawesome/free-solid-svg-icons'; @Component({ diff --git a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.ts b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.ts index f0d1016906e0..22705770daae 100644 --- a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.ts +++ b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-header/plagiarism-header.component.ts @@ -5,7 +5,7 @@ import { PlagiarismComparison } from 'app/exercises/shared/plagiarism/types/Plag import { TextSubmissionElement } from 'app/exercises/shared/plagiarism/types/text/TextSubmissionElement'; import { ModelingSubmissionElement } from 'app/exercises/shared/plagiarism/types/modeling/ModelingSubmissionElement'; import { PlagiarismCasesService } from 'app/course/plagiarism-cases/shared/plagiarism-cases.service'; -import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-button.component'; +import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Exercise, getCourseId } from 'app/entities/exercise.model'; diff --git a/src/main/webapp/app/shared/components/confirm-autofocus-button.component.html b/src/main/webapp/app/shared/components/confirm-autofocus-button.component.html new file mode 100644 index 000000000000..74820d032ac4 --- /dev/null +++ b/src/main/webapp/app/shared/components/confirm-autofocus-button.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/webapp/app/shared/components/confirm-autofocus-button.component.ts b/src/main/webapp/app/shared/components/confirm-autofocus-button.component.ts index c30f456fd773..035dd5a1516e 100644 --- a/src/main/webapp/app/shared/components/confirm-autofocus-button.component.ts +++ b/src/main/webapp/app/shared/components/confirm-autofocus-button.component.ts @@ -1,23 +1,12 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from '@angular/core'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { htmlForMarkdown } from 'app/shared/util/markdown.conversion.util'; - -@Component({ - templateUrl: './confirm-autofocus-modal.component.html', -}) -export class ConfirmAutofocusModalComponent { - title: string; - text: string; - translateText: boolean; - textIsMarkdown: boolean; - - constructor(public modal: NgbActiveModal) {} -} +import { ConfirmAutofocusModalComponent } from 'app/shared/components/confirm-autofocus-modal.component'; @Component({ selector: 'jhi-confirm-button', - template: ` `, + templateUrl: './confirm-autofocus-button.component.html', }) export class ConfirmAutofocusButtonComponent { @Input() icon: IconProp; @@ -33,13 +22,18 @@ export class ConfirmAutofocusButtonComponent { @Output() onConfirm = new EventEmitter(); @Output() onCancel = new EventEmitter(); + @ViewChild('content') content?: TemplateRef; + constructor(private modalService: NgbModal) {} /** * open confirmation modal with text and title */ onOpenConfirmationModal() { - const modalRef: NgbModalRef = this.modalService.open(ConfirmAutofocusModalComponent as Component, { size: 'lg', backdrop: 'static' }); + const modalRef: NgbModalRef = this.modalService.open(ConfirmAutofocusModalComponent, { + size: 'lg', + backdrop: 'static', + }); if (this.textIsMarkdown === true) { modalRef.componentInstance.text = htmlForMarkdown(this.confirmationText); modalRef.componentInstance.textIsMarkdown = true; @@ -53,6 +47,7 @@ export class ConfirmAutofocusButtonComponent { } else { modalRef.componentInstance.translateText = false; } + modalRef.componentInstance.contentRef = this.content; modalRef.result.then( () => { this.onConfirm.emit(); diff --git a/src/main/webapp/app/shared/components/confirm-autofocus-modal.component.html b/src/main/webapp/app/shared/components/confirm-autofocus-modal.component.html index 641ee07db38d..81b6dae4157f 100644 --- a/src/main/webapp/app/shared/components/confirm-autofocus-modal.component.html +++ b/src/main/webapp/app/shared/components/confirm-autofocus-modal.component.html @@ -2,11 +2,15 @@
- -