diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamRescheduledEvent.java b/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamRescheduledEvent.java new file mode 100644 index 000000000000..28270499260f --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamRescheduledEvent.java @@ -0,0 +1,44 @@ +package de.tum.cit.aet.artemis.exam.domain.event; + +import java.time.ZonedDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +import de.tum.cit.aet.artemis.exam.dto.examevent.ExamRescheduledEventDTO; + +/** + * An event indicating that the exam's start and end dates have been rescheduled. The working time remains unchanged in such cases. + */ +@Entity +@DiscriminatorValue(value = "R") +public class ExamRescheduledEvent extends ExamLiveEvent { + + @Column(name = "new_start_date") + private ZonedDateTime newStartDate; + + @Column(name = "new_end_date") + private ZonedDateTime newEndDate; + + public ZonedDateTime getNewStartDate() { + return newStartDate; + } + + public void setNewStartDate(ZonedDateTime newStartDate) { + this.newStartDate = newStartDate; + } + + public ZonedDateTime getNewEndDate() { + return newEndDate; + } + + public void setNewEndDate(ZonedDateTime newEndDate) { + this.newEndDate = newEndDate; + } + + @Override + public ExamRescheduledEventDTO asDTO() { + return new ExamRescheduledEventDTO(this.getId(), this.getCreatedBy(), this.getCreatedDate(), newStartDate, newEndDate); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamLiveEventBaseDTO.java b/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamLiveEventBaseDTO.java index 6df43972b309..3b61be7f7e43 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamLiveEventBaseDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamLiveEventBaseDTO.java @@ -17,6 +17,7 @@ @JsonSubTypes.Type(value = WorkingTimeUpdateEventDTO.class, name = "workingTimeUpdate"), @JsonSubTypes.Type(value = ExamAttendanceCheckEventDTO.class, name = "examAttendanceCheck"), @JsonSubTypes.Type(value = ProblemStatementUpdateEventDTO.class, name = "problemStatementUpdate"), + @JsonSubTypes.Type(value = ExamRescheduledEventDTO.class, name = "examRescheduled"), }) // @formatter:on public interface ExamLiveEventBaseDTO { diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamRescheduledEventDTO.java b/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamRescheduledEventDTO.java new file mode 100644 index 000000000000..204e00334567 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamRescheduledEventDTO.java @@ -0,0 +1,16 @@ +package de.tum.cit.aet.artemis.exam.dto.examevent; + +import java.time.Instant; +import java.time.ZonedDateTime; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.exam.domain.event.ExamRescheduledEvent; + +/** + * A DTO for the {@link ExamRescheduledEvent} entity. + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record ExamRescheduledEventDTO(Long id, String createdBy, Instant createdDate, ZonedDateTime newStartDate, ZonedDateTime newEndDate) implements ExamLiveEventBaseDTO { + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java index 31ee3dbf9e80..074a3e869a33 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamLiveEventsService.java @@ -12,6 +12,7 @@ import de.tum.cit.aet.artemis.exam.domain.StudentExam; import de.tum.cit.aet.artemis.exam.domain.event.ExamAttendanceCheckEvent; import de.tum.cit.aet.artemis.exam.domain.event.ExamLiveEvent; +import de.tum.cit.aet.artemis.exam.domain.event.ExamRescheduledEvent; import de.tum.cit.aet.artemis.exam.domain.event.ExamWideAnnouncementEvent; import de.tum.cit.aet.artemis.exam.domain.event.ProblemStatementUpdateEvent; import de.tum.cit.aet.artemis.exam.domain.event.WorkingTimeUpdateEvent; @@ -129,6 +130,28 @@ public void createAndSendWorkingTimeUpdateEvent(StudentExam studentExam, int new this.storeAndDistributeLiveExamEvent(event); } + /** + * Send an exam rescheduled update to the specified student. + * + * @param studentExam The student exam the dates rescheduled for + * @param sentBy The user who performed the update + */ + public void createAndSendExamRescheduledEvent(StudentExam studentExam, User sentBy) { + var event = new ExamRescheduledEvent(); + + // Common fields + event.setExamId(studentExam.getExam().getId()); + event.setStudentExamId(studentExam.getId()); + event.setCreatedBy(sentBy.getName()); + + // Specific fields + event.setNewStartDate(studentExam.getExam().getStartDate()); + event.setNewEndDate(studentExam.getExam().getEndDate()); + + this.storeAndDistributeLiveExamEvent(event); + + } + /** * Send a problem statement update to all affected students. * diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java index fe337ea23d5f..1fe1de664ed6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/service/ExamService.java @@ -1445,9 +1445,17 @@ public void updateStudentExamsAndRescheduleExercises(Exam exam, int originalExam studentExam.setWorkingTime(adjustedWorkingTime); } - // NOTE: if the exam is already visible, notify the student about the working time change + // NOTE: If the exam is already visible, notify the student about any changes to the working time or exam schedule if (now.isAfter(exam.getVisibleDate())) { - examLiveEventsService.createAndSendWorkingTimeUpdateEvent(studentExam, studentExam.getWorkingTime(), originalStudentWorkingTime, true, instructor); + // If the old working time equals the new working time, the exam has been rescheduled + if (studentExam.getWorkingTime().equals(originalStudentWorkingTime)) { + if (now.isBefore(exam.getStartDate())) { + examLiveEventsService.createAndSendExamRescheduledEvent(studentExam, instructor); + } + } + else { + examLiveEventsService.createAndSendWorkingTimeUpdateEvent(studentExam, studentExam.getWorkingTime(), originalStudentWorkingTime, true, instructor); + } } } studentExamRepository.saveAll(studentExams); diff --git a/src/main/resources/config/liquibase/changelog/20241211131711_changelog.xml b/src/main/resources/config/liquibase/changelog/20241211131711_changelog.xml new file mode 100644 index 000000000000..e439c2cc21f4 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241211131711_changelog.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 1dbecdb3ea97..7f9d90a911a1 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -42,6 +42,7 @@ + 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 610798664c76..1f6fb1c28962 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 @@ -204,8 +204,9 @@ export class ExamUpdateComponent implements OnInit, OnDestroy { */ handleSubmit() { const datesChanged = !(this.exam.startDate?.isSame(this.originalStartDate) && this.exam.endDate?.isSame(this.originalEndDate)); + const workingTimeChanged = this.oldWorkingTime !== this.newWorkingTime; - if (datesChanged && this.isOngoingExam) { + if (datesChanged && workingTimeChanged && 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.message'); diff --git a/src/main/webapp/app/exam/participate/events/exam-live-events-button.component.ts b/src/main/webapp/app/exam/participate/events/exam-live-events-button.component.ts index 8659685258a2..4ead29a3a9f7 100644 --- a/src/main/webapp/app/exam/participate/events/exam-live-events-button.component.ts +++ b/src/main/webapp/app/exam/participate/events/exam-live-events-button.component.ts @@ -12,8 +12,14 @@ export const USER_DISPLAY_RELEVANT_EVENTS = [ ExamLiveEventType.WORKING_TIME_UPDATE, ExamLiveEventType.EXAM_ATTENDANCE_CHECK, ExamLiveEventType.PROBLEM_STATEMENT_UPDATE, + ExamLiveEventType.EXAM_RESCHEDULED, +]; +export const USER_DISPLAY_RELEVANT_EVENTS_REOPEN = [ + ExamLiveEventType.EXAM_WIDE_ANNOUNCEMENT, + ExamLiveEventType.WORKING_TIME_UPDATE, + ExamLiveEventType.PROBLEM_STATEMENT_UPDATE, + ExamLiveEventType.EXAM_RESCHEDULED, ]; -export const USER_DISPLAY_RELEVANT_EVENTS_REOPEN = [ExamLiveEventType.EXAM_WIDE_ANNOUNCEMENT, ExamLiveEventType.WORKING_TIME_UPDATE, ExamLiveEventType.PROBLEM_STATEMENT_UPDATE]; @Component({ selector: 'jhi-exam-live-events-button', diff --git a/src/main/webapp/app/exam/participate/exam-participation-live-events.service.ts b/src/main/webapp/app/exam/participate/exam-participation-live-events.service.ts index 63e5c7cf0ec8..672181903f86 100644 --- a/src/main/webapp/app/exam/participate/exam-participation-live-events.service.ts +++ b/src/main/webapp/app/exam/participate/exam-participation-live-events.service.ts @@ -20,6 +20,7 @@ export enum ExamLiveEventType { WORKING_TIME_UPDATE = 'workingTimeUpdate', EXAM_ATTENDANCE_CHECK = 'examAttendanceCheck', PROBLEM_STATEMENT_UPDATE = 'problemStatementUpdate', + EXAM_RESCHEDULED = 'examRescheduled', } export type ExamLiveEvent = { @@ -52,6 +53,11 @@ export type ProblemStatementUpdateEvent = ExamLiveEvent & { exerciseName: string; }; +export type ExamRescheduledEvent = ExamLiveEvent & { + newStartDate: dayjs.Dayjs; + newEndDate: dayjs.Dayjs; +}; + @Injectable({ providedIn: 'root' }) export class ExamParticipationLiveEventsService { private courseId?: number; @@ -186,6 +192,10 @@ export class ExamParticipationLiveEventsService { this.events = events; this.events.forEach((event) => { event.createdDate = convertDateFromServer(event.createdDate)!; + // The "Exam Rescheduled" event should not trigger the event overlay if the student navigates to the exam page after it has been sent + if (event.eventType === ExamLiveEventType.EXAM_RESCHEDULED) { + this.acknowledgeEvent(event, true); + } }); // Replay events so unacknowledged events can be processed diff --git a/src/main/webapp/app/exam/participate/exam-participation.component.ts b/src/main/webapp/app/exam/participate/exam-participation.component.ts index 7871ce88d681..010592a0f855 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.component.ts +++ b/src/main/webapp/app/exam/participate/exam-participation.component.ts @@ -39,12 +39,14 @@ import { ExamManagementService } from 'app/exam/manage/exam-management.service'; import { ExamLiveEventType, ExamParticipationLiveEventsService, + ExamRescheduledEvent, ProblemStatementUpdateEvent, WorkingTimeUpdateEvent, } from 'app/exam/participate/exam-participation-live-events.service'; import { ExamExerciseUpdateService } from 'app/exam/manage/exam-exercise-update.service'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { SidebarCardElement, SidebarData } from 'app/types/sidebar'; +import { convertDateFromServer } from 'app/utils/date.utils'; type GenerateParticipationStatus = 'generating' | 'failed' | 'success'; @@ -102,6 +104,7 @@ export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentC errorSubscription: Subscription; websocketSubscription?: Subscription; workingTimeUpdateEventsSubscription?: Subscription; + examRescheduledEventsSubscription?: Subscription; problemStatementUpdateEventsSubscription?: Subscription; profileSubscription?: Subscription; @@ -316,6 +319,7 @@ export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentC }); } }); + this.examRescheduledEventsSubscription?.unsubscribe(); this.subscribeToProblemStatementUpdates(); this.initializeOverviewPage(); } @@ -576,6 +580,7 @@ export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentC this.errorSubscription.unsubscribe(); this.websocketSubscription?.unsubscribe(); this.workingTimeUpdateEventsSubscription?.unsubscribe(); + this.examRescheduledEventsSubscription?.unsubscribe(); this.problemStatementUpdateEventsSubscription?.unsubscribe(); this.examParticipationService.resetExamLayout(); this.profileSubscription?.unsubscribe(); @@ -589,6 +594,9 @@ export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentC if (!this.exam.testExam) { this.initIndividualEndDates(this.exam.startDate!); } + if (this.serverDateService.now().isBefore(this.exam.startDate)) { + this.subscribeToExamRescheduledEvents(); + } // only show the summary if the student was able to submit on time. if (this.isOver() && this.studentExam.submitted) { @@ -655,6 +663,21 @@ export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentC }); } + private subscribeToExamRescheduledEvents() { + if (this.examRescheduledEventsSubscription) { + this.examRescheduledEventsSubscription.unsubscribe(); + } + this.examRescheduledEventsSubscription = this.liveEventsService.observeNewEventsAsSystem([ExamLiveEventType.EXAM_RESCHEDULED]).subscribe((event: ExamRescheduledEvent) => { + // Create new object to make change detection work, otherwise the dates will not update + const startDate = convertDateFromServer(event.newStartDate); + const endDate = convertDateFromServer(event.newEndDate); + this.exam = { ...this.exam, startDate: startDate, endDate: endDate }; + this.individualStudentEndDate = dayjs(this.exam.startDate).add(this.studentExam.workingTime!, 'seconds'); + this.individualStudentEndDateWithGracePeriod = this.individualStudentEndDate.clone().add(this.exam.gracePeriod!, 'seconds'); + this.liveEventsService.acknowledgeEvent(event, false); + }); + } + private subscribeToProblemStatementUpdates() { if (this.problemStatementUpdateEventsSubscription) { this.problemStatementUpdateEventsSubscription.unsubscribe(); diff --git a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts index 3f02da6bab2b..0ce739dca607 100644 --- a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts +++ b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { InformationBox, InformationBoxComponent, InformationBoxContent } from 'app/shared/information-box/information-box.component'; @@ -14,7 +14,7 @@ import { SafeHtml } from '@angular/platform-browser'; imports: [ArtemisSharedModule, ArtemisSharedComponentModule, InformationBoxComponent, ArtemisExamSharedModule], templateUrl: './exam-start-information.component.html', }) -export class ExamStartInformationComponent implements OnInit { +export class ExamStartInformationComponent implements OnInit, OnChanges { examInformationBoxData: InformationBox[] = []; @Input() exam: Exam; @@ -32,6 +32,23 @@ export class ExamStartInformationComponent implements OnInit { gracePeriodInMinutes?: number; ngOnInit(): void { + this.updateInformationBoxes(); + } + + ngOnChanges(): void { + this.updateInformationBoxes(); + } + + buildInformationBox(boxTitle: string, boxContent: InformationBoxContent, isContentComponent = false): InformationBox { + const examInformationBoxData: InformationBox = { + title: boxTitle ?? '', + content: boxContent, + isContentComponent: isContentComponent, + }; + return examInformationBoxData; + } + + private updateInformationBoxes(): void { this.totalPoints = this.exam.examMaxPoints; this.totalWorkingTimeInMinutes = Math.floor(this.exam.workingTime! / 60); this.moduleNumber = this.exam.moduleNumber; @@ -42,18 +59,10 @@ export class ExamStartInformationComponent implements OnInit { this.startDate = this.exam.startDate; this.gracePeriodInMinutes = Math.floor(this.exam.gracePeriod! / 60); + this.examInformationBoxData = []; this.prepareInformationBoxData(); } - buildInformationBox(boxTitle: string, boxContent: InformationBoxContent, isContentComponent = false): InformationBox { - const examInformationBoxData: InformationBox = { - title: boxTitle ?? '', - content: boxContent, - isContentComponent: isContentComponent, - }; - return examInformationBoxData; - } - prepareInformationBoxData(): void { if (this.moduleNumber) { const boxContentModuleNumber: InformationBoxContent = { 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 06bd840dcf5f..4334f11a645d 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 @@ -71,6 +71,15 @@ } } + @case (ExamLiveEventType.EXAM_RESCHEDULED) { +
+
+
+ } }
diff --git a/src/main/webapp/app/exam/shared/events/exam-live-event.component.scss b/src/main/webapp/app/exam/shared/events/exam-live-event.component.scss index d617a85c2a60..29eafa663677 100644 --- a/src/main/webapp/app/exam/shared/events/exam-live-event.component.scss +++ b/src/main/webapp/app/exam/shared/events/exam-live-event.component.scss @@ -44,6 +44,18 @@ border-color: var(--artemis-alert-info-border); } + &.examRescheduled { + background-color: var(--artemis-alert-warning-background); + border-color: var(--artemis-alert-warning-border); + + .wt-title { + font-weight: bold; + font-size: 1.5em; + text-align: center; + margin-bottom: 15px; + } + } + .headline { display: flex; justify-content: space-between; diff --git a/src/main/webapp/app/exam/shared/events/exam-live-event.component.ts b/src/main/webapp/app/exam/shared/events/exam-live-event.component.ts index ae2422c4f91f..f81b0614d255 100644 --- a/src/main/webapp/app/exam/shared/events/exam-live-event.component.ts +++ b/src/main/webapp/app/exam/shared/events/exam-live-event.component.ts @@ -4,6 +4,7 @@ import { ExamAttendanceCheckEvent, ExamLiveEvent, ExamLiveEventType, + ExamRescheduledEvent, ExamWideAnnouncementEvent, ProblemStatementUpdateEvent, WorkingTimeUpdateEvent, @@ -50,6 +51,10 @@ export class ExamLiveEventComponent { return this.event as ProblemStatementUpdateEvent; } + get examRescheduledEvent(): ExamRescheduledEvent { + return this.event as ExamRescheduledEvent; + } + acknowledgeEvent() { this.onAcknowledge.emit(this.event); } diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index db2c485e9ed2..80b1309e4b08 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -187,7 +187,8 @@ "examWideAnnouncement": "Klausurweite Ankündigung", "workingTimeUpdate": "Bearbeitungszeit Aktualisiert", "examAttendanceCheck": "Anwesenheitskontrolle", - "problemStatementUpdate": "Aufgabenstellung Aktualisiert" + "problemStatementUpdate": "Aufgabenstellung Aktualisiert", + "examRescheduled": "Prüfung verschoben" }, "from": "von", "acknowledge": "Bestätigen", @@ -206,6 +207,9 @@ "problemStatementUpdate": { "description": "Die Aufgabenstellung der Aufgabe '{{ exerciseName }}' wurde aktualisiert. Bitte öffne die Aufgabe, um die Änderungen zu sehen.", "instructorMessage": "Lehrkraft Nachricht:" + }, + "examRescheduled": { + "description": "Die Prüfung wurde verschoben.
Die Prüfung findet vom {{ startDate }} bis zum {{ endDate }} statt.
Die Bearbeitungszeit wurde nicht geändert." } } } diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index 6b853a1de896..7028433b308a 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -187,7 +187,8 @@ "examWideAnnouncement": "Exam-Wide Announcement", "workingTimeUpdate": "Working Time Update", "examAttendanceCheck": "Attendance Check", - "problemStatementUpdate": "Problem Statement Update" + "problemStatementUpdate": "Problem Statement Update", + "examRescheduled": "Exam rescheduled" }, "from": "from", "acknowledge": "Acknowledge", @@ -206,6 +207,9 @@ "problemStatementUpdate": { "description": "The problem statement of the exercise '{{ exerciseName }}' was updated. Please open the exercise to see the changes.", "instructorMessage": "Instructor's message:" + }, + "examRescheduled": { + "description": "The exam has been rescheduled.
The exam will take place from {{ startDate }} to {{ endDate }}.
The working time has not been affected." } } } diff --git a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts index 1e4bda79525a..883beaa22739 100644 --- a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts @@ -152,7 +152,10 @@ describe('ExamParticipationComponent', () => { courseStorageService = TestBed.inject(CourseStorageService); examManagementService = TestBed.inject(ExamManagementService); fixture.detectChanges(); - comp.exam = new Exam(); + const exam = new Exam(); + exam.startDate = dayjs(); + comp.exam = exam; + jest.spyOn(artemisServerDateService, 'now').mockReturnValue(dayjs()); }); }); @@ -560,6 +563,51 @@ describe('ExamParticipationComponent', () => { }); }); + describe('websocket exam rescheduled subscription', () => { + const startDate = dayjs().add(5, 'minutes'); + const endDate = dayjs().add(10, 'minutes'); + const exam = new Exam(); + exam.startDate = dayjs().add(5, 'minutes'); + exam.endDate = dayjs().add(10, 'minutes'); + const studentExam = { id: 3, workingTime: 420, numberOfExamSessions: 0, exam: exam }; + + it('should correctly postpone exam', () => { + const newStartDate = startDate.add(10, 'minutes'); + const newEndDate = endDate.add(10, 'minutes'); + + const event = { + newStartDate: newStartDate, + newEndDate: newEndDate, + } as any as ExamLiveEvent; + + jest.spyOn(examParticipationLiveEventsService, 'observeNewEventsAsSystem').mockReturnValue(of(event)); + const ackSpy = jest.spyOn(examParticipationLiveEventsService, 'acknowledgeEvent'); + comp.handleStudentExam(studentExam); + + expect(comp.exam.startDate).toStrictEqual(newStartDate); + expect(comp.exam.endDate).toStrictEqual(newEndDate); + expect(ackSpy).toHaveBeenCalledExactlyOnceWith(event, false); + }); + + it('should correctly bring forward exam', () => { + const newStartDate = startDate.subtract(10, 'minutes'); + const newEndDate = endDate.subtract(10, 'minutes'); + + const event = { + newStartDate: newStartDate, + newEndDate: newEndDate, + } as any as ExamLiveEvent; + + jest.spyOn(examParticipationLiveEventsService, 'observeNewEventsAsSystem').mockReturnValue(of(event)); + const ackSpy = jest.spyOn(examParticipationLiveEventsService, 'acknowledgeEvent'); + comp.handleStudentExam(studentExam); + + expect(comp.exam.startDate).toStrictEqual(newStartDate); + expect(comp.exam.endDate).toStrictEqual(newEndDate); + expect(ackSpy).toHaveBeenCalledExactlyOnceWith(event, false); + }); + }); + describe('websocket problem statement update subscription', () => { beforeEach(() => { comp.studentExam = new StudentExam();