From 4e706af1555338638decbab3237f3d4a59dba2a8 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Thu, 12 Dec 2024 17:43:18 +0100 Subject: [PATCH 01/23] add exam shifted event entity and dto --- .../exam/domain/event/ExamShiftedEvent.java | 65 +++++++++++++++++++ .../dto/examevent/ExamShiftedEventDTO.java | 13 ++++ 2 files changed, 78 insertions(+) create mode 100644 src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamShiftedEvent.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamShiftedEventDTO.java diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamShiftedEvent.java b/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamShiftedEvent.java new file mode 100644 index 000000000000..0c6528914d90 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamShiftedEvent.java @@ -0,0 +1,65 @@ +package de.tum.cit.aet.artemis.exam.domain.event; + +import java.time.Instant; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +import de.tum.cit.aet.artemis.exam.dto.examevent.ExamShiftedEventDTO; + +/** + * An event indicating that exam start and end dates were shifted for a specific student exam. In case of this event working time doesn't change. + */ +@Entity +@DiscriminatorValue(value = "S") +public class ExamShiftedEvent extends ExamLiveEvent { + + /** + * The new start date + */ + @Column(name = "newStartDate") + private Instant newStartDate; + + /** + * The new end date + */ + @Column(name = "newEndDate") + private Instant newEndDate; + + /** + * While the event always contains a new start and end date for a specific student exam, + * this flags indicates whether the dates were updated for every student exam of the exam. + */ + @Column(name = "courseWide") + private boolean courseWide; + + public Instant getNewStartDate() { + return newStartDate; + } + + public void setNewStartDate(Instant newStartDate) { + this.newStartDate = newStartDate; + } + + public Instant getNewEndDate() { + return newEndDate; + } + + public void setNewEndDate(Instant newEndDate) { + this.newEndDate = newEndDate; + } + + public boolean isCourseWide() { + return courseWide; + } + + public void setCourseWide(boolean courseWide) { + this.courseWide = courseWide; + } + + @Override + public ExamShiftedEventDTO asDTO() { + return new ExamShiftedEventDTO(this.getId(), this.getCreatedBy(), this.getCreatedDate(), newStartDate, newEndDate, courseWide); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamShiftedEventDTO.java b/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamShiftedEventDTO.java new file mode 100644 index 000000000000..7189afae9d5b --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamShiftedEventDTO.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.exam.dto.examevent; + +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * A DTO for the {@link de.tum.cit.aet.artemis.exam.domain.event.ExamShiftedEvent} entity. + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record ExamShiftedEventDTO(Long id, String createdBy, Instant createdDate, Instant newStartDate, Instant newEndDate, boolean courseWide) implements ExamLiveEventBaseDTO { + +} From adef7fcdc138ff4266808798f6ff23f1dda0b65c Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sat, 14 Dec 2024 17:22:02 +0100 Subject: [PATCH 02/23] rename entity and dto --- .../domain/event/ExamRescheduledEvent.java | 50 ++++++++++++++ .../exam/domain/event/ExamShiftedEvent.java | 65 ------------------- .../examevent/ExamRescheduledEventDTO.java | 16 +++++ .../dto/examevent/ExamShiftedEventDTO.java | 13 ---- 4 files changed, 66 insertions(+), 78 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamRescheduledEvent.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamShiftedEvent.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamRescheduledEventDTO.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamShiftedEventDTO.java 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..9ff950c142d0 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamRescheduledEvent.java @@ -0,0 +1,50 @@ +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 exam start and end dates were rescheduled for a specific student exam. In case of this event working time doesn't change. + */ +@Entity +@DiscriminatorValue(value = "R") +public class ExamRescheduledEvent extends ExamLiveEvent { + + /** + * The new start date + */ + @Column(name = "newStartDate") + private ZonedDateTime newStartDate; + + /** + * The new end date + */ + @Column(name = "newEndDate") + 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/domain/event/ExamShiftedEvent.java b/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamShiftedEvent.java deleted file mode 100644 index 0c6528914d90..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/exam/domain/event/ExamShiftedEvent.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.tum.cit.aet.artemis.exam.domain.event; - -import java.time.Instant; - -import jakarta.persistence.Column; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; - -import de.tum.cit.aet.artemis.exam.dto.examevent.ExamShiftedEventDTO; - -/** - * An event indicating that exam start and end dates were shifted for a specific student exam. In case of this event working time doesn't change. - */ -@Entity -@DiscriminatorValue(value = "S") -public class ExamShiftedEvent extends ExamLiveEvent { - - /** - * The new start date - */ - @Column(name = "newStartDate") - private Instant newStartDate; - - /** - * The new end date - */ - @Column(name = "newEndDate") - private Instant newEndDate; - - /** - * While the event always contains a new start and end date for a specific student exam, - * this flags indicates whether the dates were updated for every student exam of the exam. - */ - @Column(name = "courseWide") - private boolean courseWide; - - public Instant getNewStartDate() { - return newStartDate; - } - - public void setNewStartDate(Instant newStartDate) { - this.newStartDate = newStartDate; - } - - public Instant getNewEndDate() { - return newEndDate; - } - - public void setNewEndDate(Instant newEndDate) { - this.newEndDate = newEndDate; - } - - public boolean isCourseWide() { - return courseWide; - } - - public void setCourseWide(boolean courseWide) { - this.courseWide = courseWide; - } - - @Override - public ExamShiftedEventDTO asDTO() { - return new ExamShiftedEventDTO(this.getId(), this.getCreatedBy(), this.getCreatedDate(), newStartDate, newEndDate, courseWide); - } -} 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/dto/examevent/ExamShiftedEventDTO.java b/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamShiftedEventDTO.java deleted file mode 100644 index 7189afae9d5b..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/exam/dto/examevent/ExamShiftedEventDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.tum.cit.aet.artemis.exam.dto.examevent; - -import java.time.Instant; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * A DTO for the {@link de.tum.cit.aet.artemis.exam.domain.event.ExamShiftedEvent} entity. - */ -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public record ExamShiftedEventDTO(Long id, String createdBy, Instant createdDate, Instant newStartDate, Instant newEndDate, boolean courseWide) implements ExamLiveEventBaseDTO { - -} From 71e0381e1a6725856686b5426d9fba9059fd089a Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sat, 14 Dec 2024 17:22:38 +0100 Subject: [PATCH 03/23] add migration --- .../liquibase/changelog/20241211131711_changelog.xml | 11 +++++++++++ src/main/resources/config/liquibase/master.xml | 1 + 2 files changed, 12 insertions(+) create mode 100644 src/main/resources/config/liquibase/changelog/20241211131711_changelog.xml 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 9dd06528e6f3..40a3dd91f0d5 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -41,6 +41,7 @@ + From 031e47b021140401fc18f753d3a8359c8bbf07f6 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sat, 14 Dec 2024 17:23:03 +0100 Subject: [PATCH 04/23] add dto mapping --- .../cit/aet/artemis/exam/dto/examevent/ExamLiveEventBaseDTO.java | 1 + 1 file changed, 1 insertion(+) 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 { From 42932354012e1a745ad5c7feed18a9a2dd05a7fd Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sat, 14 Dec 2024 17:27:18 +0100 Subject: [PATCH 05/23] send exam rescheduled events --- .../exam/service/ExamLiveEventsService.java | 17 +++++++++++++++++ .../aet/artemis/exam/service/ExamService.java | 12 ++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) 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..a1a0cc27ca1e 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,22 @@ public void createAndSendWorkingTimeUpdateEvent(StudentExam studentExam, int new this.storeAndDistributeLiveExamEvent(event); } + 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..22bdb3a728db 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 changes to the working time or the exam rescheduling 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); From 8ce7d9167cce2b5a28406716240331c3d7f5600c Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 18:05:10 +0100 Subject: [PATCH 06/23] add new event type --- .../exam-participation-live-events.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 From 476a3c0f829e87dc1f864e1647b2448e15837471 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 18:06:23 +0100 Subject: [PATCH 07/23] add new event type --- .../events/exam-live-events-button.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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', From d358da5c6bff15201be0028e9b5a22f0a63f872f Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 18:11:12 +0100 Subject: [PATCH 08/23] subscribe to exam rescheduled events --- .../exam-participation.component.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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(); From 2f30560334da23ea49cb5786dbbc851acaf765e0 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 18:14:32 +0100 Subject: [PATCH 09/23] add new event html --- .../app/exam/shared/events/exam-live-event.component.html | 5 +++++ 1 file changed, 5 insertions(+) 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..c613fb521a23 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,11 @@ } } + @case (ExamLiveEventType.EXAM_RESCHEDULED) { +
+
+
+ } }
From 7747c7f807172b7a4e8a04e20c6eebf570527de8 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 18:14:44 +0100 Subject: [PATCH 10/23] add css for new event --- .../app/exam/shared/events/exam-live-event.component.scss | 5 +++++ 1 file changed, 5 insertions(+) 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..a1c3a98eaa5f 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,11 @@ border-color: var(--artemis-alert-info-border); } + &.examRescheduled { + background-color: var(--artemis-alert-warning-background); + border-color: var(--artemis-alert-warning-border); + } + .headline { display: flex; justify-content: space-between; From 84a3254869cbedfd9eba4903b57d6984ec09e0be Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 19:43:50 +0100 Subject: [PATCH 11/23] add translations --- src/main/webapp/i18n/de/exam.json | 6 +++++- src/main/webapp/i18n/en/exam.json | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index 34cea3d9900d..76e7dccf9172 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." } } } diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index 374506d757af..8f9ced134aa9 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 }}." } } } From 73b2c46e20853d2dd6b29a889e8612ff83a875c2 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 19:44:06 +0100 Subject: [PATCH 12/23] fix date format --- .../app/exam/shared/events/exam-live-event.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 c613fb521a23..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 @@ -73,7 +73,11 @@ } @case (ExamLiveEventType.EXAM_RESCHEDULED) {
-
+
} } From 68795d7ce2468be360bb18a291972bdb3daf2e24 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 19:44:44 +0100 Subject: [PATCH 13/23] add getter for exam rescheduled event --- .../app/exam/shared/events/exam-live-event.component.ts | 5 +++++ 1 file changed, 5 insertions(+) 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); } From 990198f12d1848eda1efddea0bcdc235b8b6cdc2 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 19:45:10 +0100 Subject: [PATCH 14/23] fix issues with exam information not being updated --- .../exam-start-information.component.ts | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) 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 = { From e647c83ec5d02c9310339ae70e9eb59f4af3d66c Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 20:26:28 +0100 Subject: [PATCH 15/23] improve css --- .../app/exam/shared/events/exam-live-event.component.scss | 7 +++++++ 1 file changed, 7 insertions(+) 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 a1c3a98eaa5f..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 @@ -47,6 +47,13 @@ &.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 { From bd26dfab4c98e2fc46f14ef2fb877947e1f2768a Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 20:26:46 +0100 Subject: [PATCH 16/23] improve translations --- src/main/webapp/i18n/de/exam.json | 2 +- src/main/webapp/i18n/en/exam.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index 76e7dccf9172..17992cd1ae23 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -209,7 +209,7 @@ "instructorMessage": "Lehrkraft Nachricht:" }, "examRescheduled": { - "description": "Die Prüfung wurde verschoben. Die Prüfung findet vom {{ startDate }} bis zum {{ endDate }} statt." + "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 8f9ced134aa9..caff39493067 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -209,7 +209,7 @@ "instructorMessage": "Instructor's message:" }, "examRescheduled": { - "description": "The exam has been rescheduled. The exam will take place from {{ startDate }} to {{ endDate }}." + "description": "The exam has been rescheduled.
The exam will take place from {{ startDate }} to {{ endDate }}.
The working time has not been affected." } } } From 1cb77b4d9db268850fd22d68d25e3b211f3adb8c Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 20:38:30 +0100 Subject: [PATCH 17/23] improve comments --- .../cit/aet/artemis/exam/domain/event/ExamRescheduledEvent.java | 2 +- .../java/de/tum/cit/aet/artemis/exam/service/ExamService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 9ff950c142d0..ffbb70ddf42d 100644 --- 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 @@ -9,7 +9,7 @@ import de.tum.cit.aet.artemis.exam.dto.examevent.ExamRescheduledEventDTO; /** - * An event indicating that exam start and end dates were rescheduled for a specific student exam. In case of this event working time doesn't change. + * 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") 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 22bdb3a728db..6cd66e6eff1a 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,7 +1445,7 @@ public void updateStudentExamsAndRescheduleExercises(Exam exam, int originalExam studentExam.setWorkingTime(adjustedWorkingTime); } - // NOTE: If the exam is already visible, notify the student about changes to the working time or the exam rescheduling + // "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())) { // If the old working time equals the new working time, the exam has been rescheduled if (studentExam.getWorkingTime().equals(originalStudentWorkingTime)) { From 97ce3791d93920996869c50641c0e621069936c9 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Sun, 15 Dec 2024 21:05:31 +0100 Subject: [PATCH 18/23] add javadoc --- .../cit/aet/artemis/exam/service/ExamLiveEventsService.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 a1a0cc27ca1e..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 @@ -130,6 +130,12 @@ 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(); From 3b8eb1b6089845033d215ca4d204c8656db8d28e Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 16 Dec 2024 00:44:43 +0100 Subject: [PATCH 19/23] fix tests --- .../exam/participate/exam-participation.component.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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..fb26b94ab865 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()); }); }); From 39d63d1b1bfaed131804209c69fc2cc278c7bed3 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Mon, 16 Dec 2024 22:50:52 +0100 Subject: [PATCH 20/23] don't show working time change confirm modal when rescheduling exam --- src/main/webapp/app/exam/manage/exams/exam-update.component.ts | 3 ++- 1 file changed, 2 insertions(+), 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 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'); From 6777ac6230d22631d6d315c47a71833fc6399058 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 17 Dec 2024 16:32:00 +0100 Subject: [PATCH 21/23] rename columns --- .../exam/domain/event/ExamRescheduledEvent.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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 index ffbb70ddf42d..28270499260f 100644 --- 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 @@ -15,16 +15,10 @@ @DiscriminatorValue(value = "R") public class ExamRescheduledEvent extends ExamLiveEvent { - /** - * The new start date - */ - @Column(name = "newStartDate") + @Column(name = "new_start_date") private ZonedDateTime newStartDate; - /** - * The new end date - */ - @Column(name = "newEndDate") + @Column(name = "new_end_date") private ZonedDateTime newEndDate; public ZonedDateTime getNewStartDate() { From d2f0d313c32efc3ce447354eb27f65fba6c7b673 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Tue, 17 Dec 2024 18:53:48 +0100 Subject: [PATCH 22/23] remove quotes --- .../java/de/tum/cit/aet/artemis/exam/service/ExamService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6cd66e6eff1a..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,7 +1445,7 @@ public void updateStudentExamsAndRescheduleExercises(Exam exam, int originalExam studentExam.setWorkingTime(adjustedWorkingTime); } - // "NOTE: If the exam is already visible, notify the student about any changes to the working time or exam schedule." + // 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())) { // If the old working time equals the new working time, the exam has been rescheduled if (studentExam.getWorkingTime().equals(originalStudentWorkingTime)) { From d17d7f88710ada4fce4a929187354fe0018ec481 Mon Sep 17 00:00:00 2001 From: Michal Kawka Date: Wed, 18 Dec 2024 20:45:14 +0100 Subject: [PATCH 23/23] improve coverage --- .../exam-participation.component.spec.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) 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 fb26b94ab865..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 @@ -563,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();