Skip to content

Commit da8c548

Browse files
authored
Add support for multiple exams at the same time (#15)
1 parent 2f5b2c8 commit da8c548

File tree

2 files changed

+80
-51
lines changed

2 files changed

+80
-51
lines changed

client/ui.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class UI {
4949
else {
5050
// No active session found, show login form or exam mode form
5151
this._loginScreen = new LoginScreenUI(auth);
52-
this._examModeScreen = new ExamModeUI(auth, this._loginScreen, null);
52+
this._examModeScreen = new ExamModeUI(auth, this._loginScreen);
5353

5454
// Subscribe to data change events, so that we can show the exam mode screen when an exam is started
5555
data.addDataChangeListener((data: DataJson | undefined) => {
@@ -119,28 +119,29 @@ export class UI {
119119
return false;
120120
}
121121

122+
// Get exams that are starting soon
122123
const examsForHost: ExamForHost[] = window.data.dataJson.exams_for_host;
123-
const ongoingExam = examsForHost.find((exam) => {
124+
const ongoingExams = examsForHost.filter((exam) => {
124125
const now = new Date();
125126
const beginAt = new Date(exam.begin_at);
126127
const beginExamModeAt = new Date(beginAt.getTime() - UI.SHOW_EXAM_MODE_MINUTES_BEFORE_BEGIN * 60 * 1000);
127128
const endAt = new Date(exam.end_at);
128129
return now >= beginExamModeAt && now < endAt;
129130
});
130131

131-
const examModeExam = this._examModeScreen?.exam;
132-
if (ongoingExam !== undefined) {
133-
if (examModeExam === null || examModeExam?.id !== ongoingExam.id) { // Only set exam mode again if the exam has changed or was not set before
132+
if (ongoingExams.length > 0) {
133+
// Only set exam mode if the exam that is starting soon is not already in the list of exam ids displayed in exam mode
134+
if (!this._examModeScreen?.examMode || !ongoingExams.some((exam) => this._examModeScreen?.examIds.includes(exam.id))) {
134135
console.log("Activating exam mode login UI");
135-
this._examModeScreen?.setExam(ongoingExam);
136+
this._examModeScreen?.enableExamMode(ongoingExams);
136137
// Exam mode screen is shown automatically by the function above
137138
}
138139
return true;
139140
}
140141
else {
141-
if (examModeExam !== null) { // Only unset exam mode if it was set before
142+
if (this._examModeScreen?.examMode) { // Only unset exam mode if it was set before
142143
console.log('Deactivating exam mode login UI');
143-
this._examModeScreen?.setExam(null);
144+
this._examModeScreen?.disableExamMode();
144145
// Login screen is shown automatically by the function above
145146
}
146147
return false;

client/uis/screens/examscreen.ts

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export class ExamModeUI extends UIScreen {
88
public static readonly EXAM_PASSWORD: string = 'exam';
99

1010
public readonly _form: UIExamModeElements;
11-
private _exam: ExamForHost | null = null;
11+
private _examMode: boolean = false;
12+
private _examIds: number[] = [];
1213
private _loginScreen: LoginScreenUI;
1314
protected _events: AuthenticatorEvents = {
1415
authenticationStart: () => {
@@ -30,7 +31,7 @@ export class ExamModeUI extends UIScreen {
3031
},
3132
};
3233

33-
public constructor(auth: Authenticator, loginUI: LoginScreenUI, exam: ExamForHost | null = null) {
34+
public constructor(auth: Authenticator, loginUI: LoginScreenUI) {
3435
super(auth);
3536

3637
// Keep a reference to the login screen so that we can show it when the exam is over
@@ -45,35 +46,46 @@ export class ExamModeUI extends UIScreen {
4546
} as UIExamModeElements;
4647

4748
this._initForm();
49+
}
4850

49-
if (exam !== null) {
50-
this.setExam(exam);
51+
/**
52+
* Enable exam mode for a list of exams currently ongoing (usually just 1).
53+
* If the array of exams is empty, nothing will happen.
54+
*/
55+
public enableExamMode(exams: ExamForHost[]): void {
56+
if (exams.length === 0) {
57+
return;
5158
}
59+
this._examMode = true;
60+
this._examIds = exams.map((exam) => exam.id);
61+
this._populateData(exams);
62+
this._loginScreen.hideForm();
63+
this.showForm();
5264
}
5365

5466
/**
55-
* Set the exam to display on the exam mode screen.
56-
* If no exam is given, the exam mode screen will be hidden and the login screen will be shown instead.
57-
* @returns true if the exam mode screen should be shown, false if the login screen is shown instead
67+
* Disable exam mode and show the default login screen instead.
5868
*/
59-
public setExam(exam: ExamForHost | null): boolean {
60-
this._exam = exam;
61-
this._populateData();
62-
63-
if (this._exam === null) {
64-
this.hideForm();
65-
this._loginScreen.showForm();
66-
return false;
67-
}
68-
else {
69-
this._loginScreen.hideForm();
70-
this.showForm();
71-
return true;
72-
}
69+
public disableExamMode(): void {
70+
this._examMode = false;
71+
this._examIds = [];
72+
this._populateData([]);
73+
this.hideForm();
74+
this._loginScreen.showForm();
75+
}
76+
77+
/**
78+
* Get whether the exam mode screen is currently displayed.
79+
*/
80+
public get examMode(): boolean {
81+
return this._examMode;
7382
}
7483

75-
public get exam(): ExamForHost | null {
76-
return this._exam;
84+
/**
85+
* Get the ids of the exams that are currently displayed on the exam mode screen.
86+
*/
87+
public get examIds(): number[] {
88+
return this._examIds;
7789
}
7890

7991
protected _initForm(): void {
@@ -82,40 +94,56 @@ export class ExamModeUI extends UIScreen {
8294
// This event gets called when the user clicks the unlock button or submits the lock screen form in any other way
8395
form.examStartButton.addEventListener('click', (event: Event) => {
8496
event.preventDefault();
85-
if (this._exam !== null) {
86-
// Always log in with the username and password given by the back-end server.
87-
// If no username and password are given, use the default username and password.
97+
if (this._examMode) {
8898
this._auth.login(ExamModeUI.EXAM_USERNAME, ExamModeUI.EXAM_PASSWORD);
8999
}
90-
else {
91-
console.error('Exam is null');
92-
window.ui.setDebugInfo('Exam is null');
93-
}
94100
});
95101
}
96102

97-
private _populateData(): void {
103+
private _populateData(examsToPopulate: ExamForHost[]): void {
98104
const form = this._form as UIExamModeElements;
99105

100-
if (this._exam === null) {
106+
if (examsToPopulate.length === 0) {
101107
// Unset text that states which exams can be started today
102108
form.examProjectsText.innerText = '';
109+
form.examStartText.innerText = 'unknown';
110+
form.examEndText.innerText = 'unknown';
103111
}
104112
else {
105-
// Populate text that states which exams can be started today
106-
const exam = window.data.dataJson?.exams.find((exam) => exam.id === this._exam?.id);
107-
if (exam === undefined) {
108-
console.error('Exam not found in data.json');
109-
window.ui.setDebugInfo('Exam not found in data.json');
113+
// Find all exams in the data.json file that match the ids in the exams variable
114+
const exams = window.data.dataJson?.exams.filter((exam) => examsToPopulate.some((examToPopulate) => exam.id === examToPopulate.id));
115+
116+
if (exams === undefined) {
117+
console.error('Failed to find exams in data.json');
118+
window.ui.setDebugInfo('Failed to find exams in data.json');
110119
return;
111120
}
112-
const projectsText = exam.projects.map((project) => project.name).join(', ');
113-
form.examProjectsText.innerText = projectsText;
114121

115-
const examStart = new Date(this._exam.begin_at);
116-
const examEnd = new Date(this._exam.end_at);
117-
form.examStartText.innerText = examStart.toLocaleTimeString("en-NL", { hour: '2-digit', minute: '2-digit' });
118-
form.examEndText.innerText = examEnd.toLocaleTimeString("en-NL", { hour: '2-digit', minute: '2-digit' });
122+
// Find the earliest start time for an exam that should be displayed right now
123+
const earliestExam = exams.reduce((earliest, exam) => {
124+
const beginAt = new Date(exam.begin_at);
125+
if (earliest === null || beginAt < earliest) {
126+
return beginAt;
127+
}
128+
return earliest;
129+
}, new Date(exams[0].begin_at));
130+
131+
// Find the latest end time for an exam that should be displayed right now
132+
const latestExam = exams.reduce((latest, exam) => {
133+
const endAt = new Date(exam.end_at);
134+
if (latest === null || endAt > latest) {
135+
return endAt;
136+
}
137+
return latest;
138+
}, new Date(exams[0].end_at));
139+
140+
// Combine all possible projects for exams that can be started right now
141+
const projectsText = exams.flatMap((exam) => exam.projects.map((project) => project.name)).join(', ');
142+
143+
// Display the projects and the time range in which the exams can be started
144+
form.examProjectsText.innerText = projectsText;
145+
form.examStartText.innerText = earliestExam.toLocaleTimeString("en-NL", { hour: '2-digit', minute: '2-digit' });
146+
form.examEndText.innerText = latestExam.toLocaleTimeString("en-NL", { hour: '2-digit', minute: '2-digit' });
119147
}
120148
}
121149

0 commit comments

Comments
 (0)