Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exam mode: Add exam rescheduled event #10026

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 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 {
coolchock marked this conversation as resolved.
Show resolved Hide resolved

/**
* The new start date
*/
@Column(name = "newStartDate")
coolchock marked this conversation as resolved.
Show resolved Hide resolved
private ZonedDateTime newStartDate;

/**
* The new end date
*/
@Column(name = "newEndDate")
coolchock marked this conversation as resolved.
Show resolved Hide resolved
private ZonedDateTime newEndDate;
coolchock marked this conversation as resolved.
Show resolved Hide resolved

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);
}
coolchock marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

}
coolchock marked this conversation as resolved.
Show resolved Hide resolved

/**
* Send a problem statement update to all affected students.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."
coolchock marked this conversation as resolved.
Show resolved Hide resolved
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="20241211131711" author="mkawka">
<addColumn tableName="exam_live_event">
<column name="new_start_date" type="datetime(3)"/>
<column name="new_end_date" type="datetime(3)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
1 change: 1 addition & 0 deletions src/main/resources/config/liquibase/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<include file="classpath:config/liquibase/changelog/20241114122713_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241119191919_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241125000900_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241211131711_changelog.xml" relativeToChangelogFile="false"/>

<!-- NOTE: please use the format "YYYYMMDDhhmmss_changelog.xml", i.e. year month day hour minutes seconds and not something else! -->
<!-- we should also stay in a chronological order! -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -102,6 +104,7 @@ export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentC
errorSubscription: Subscription;
websocketSubscription?: Subscription;
workingTimeUpdateEventsSubscription?: Subscription;
examRescheduledEventsSubscription?: Subscription;
problemStatementUpdateEventsSubscription?: Subscription;
profileSubscription?: Subscription;

Expand Down Expand Up @@ -316,6 +319,7 @@ export class ExamParticipationComponent implements OnInit, OnDestroy, ComponentC
});
}
});
this.examRescheduledEventsSubscription?.unsubscribe();
this.subscribeToProblemStatementUpdates();
this.initializeOverviewPage();
}
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@
}
</div>
}
@case (ExamLiveEventType.EXAM_RESCHEDULED) {
<div>
<div
[jhiTranslate]="'artemisApp.exam.events.messages.examRescheduled.description'"
[translateValues]="{ startDate: examRescheduledEvent.newStartDate | artemisDate, endDate: examRescheduledEvent.newEndDate | artemisDate }"
class="wt-title"
></div>
</div>
}
}
</div>
<div class="d-flex gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ExamAttendanceCheckEvent,
ExamLiveEvent,
ExamLiveEventType,
ExamRescheduledEvent,
ExamWideAnnouncementEvent,
ProblemStatementUpdateEvent,
WorkingTimeUpdateEvent,
Expand Down Expand Up @@ -50,6 +51,10 @@ export class ExamLiveEventComponent {
return this.event as ProblemStatementUpdateEvent;
}

get examRescheduledEvent(): ExamRescheduledEvent {
return this.event as ExamRescheduledEvent;
}
coolchock marked this conversation as resolved.
Show resolved Hide resolved

acknowledgeEvent() {
this.onAcknowledge.emit(this.event);
}
Expand Down
Loading
Loading