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();