Skip to content

Commit

Permalink
CSCEXAM-721 Add random delay to examination start moment
Browse files Browse the repository at this point in the history
  • Loading branch information
Matti Lupari committed Jan 30, 2024
1 parent f5aa77c commit 852c885
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 97 deletions.
1 change: 0 additions & 1 deletion conf/evolutions/default/133.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# --- !Ups
<<<<<<< HEAD
INSERT INTO permission (id, object_version, type, description) VALUES (2, 1, 2, 'can create BYOD exams')

# --- !Downs
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/enrolment/search/exam-search.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ import { ExamSearchService } from './exam-search.service';
})
export class ExamSearchComponent implements OnInit, OnDestroy {
exams: EnrolmentInfo[] = [];
filterChanged: Subject<string> = new Subject<string>();
filterChanged = new Subject<string>();
ngUnsubscribe = new Subject();
filter = { text: '' };
permissionCheck = { active: false };
Expand Down
4 changes: 2 additions & 2 deletions ui/src/app/enrolment/waiting-room/waiting-room.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ <h1 [hidden]="isUpcoming" class="student-enroll-title">
<div class="waitingroom-info">
@if (delayCounter$) {
<div class="alert alert-info me-4">
<i class="bi bi-exclamation-circle-fill"></i>
<i class="bi bi-exclamation-circle-fill me-4"></i>
{{ delayCounter$ | async }} {{ 'i18n_seconds_until_examination_starts' | translate }}
</div>
} @else if (isUpcoming()) {
<div class="alert alert-secondary me-4">
<i class="bi bi-exclamation-circle-fill"></i>
<i class="bi bi-exclamation-circle-fill me-4"></i>
{{ 'i18n_redirect_to_exam_notice' | translate }}
</div>
}
Expand Down
7 changes: 3 additions & 4 deletions ui/src/app/enrolment/waiting-room/waiting-room.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { MathJaxDirective } from '../../shared/math/math-jax.directive';
import { CourseCodeComponent } from '../../shared/miscellaneous/course-code.component';
import { TeacherListComponent } from '../../shared/user/teacher-list.component';
import type { ExamEnrolment } from '../enrolment.model';
import { Observable, interval, map, startWith, take } from 'rxjs';
import { Observable, interval, map, startWith } from 'rxjs';

type WaitingReservation = Reservation & { occasion: { startAt: string; endAt: string } };
type WaitingEnrolment = Omit<ExamEnrolment, 'reservation'> & {
Expand Down Expand Up @@ -99,14 +99,13 @@ export class WaitingRoomComponent implements OnInit, OnDestroy {

private startScheduled = () => {
window.clearTimeout(this.startTimerId);
const offset = Math.round(
const offset = Math.ceil(
DateTime.fromJSDate(this.getStart()).plus({ seconds: this.enrolment.delay }).toSeconds() -
DateTime.now().toSeconds(),
);
this.delayTimerId = window.setTimeout(this.Session.checkSession, offset);
this.delayTimerId = window.setTimeout(this.Session.checkSession, offset * 1000);
this.delayCounter$ = interval(1000).pipe(
startWith(0),
take(offset),
map((n) => offset - n),
);
};
Expand Down
113 changes: 50 additions & 63 deletions ui/src/app/examination/clock/examination-clock.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,110 +12,97 @@
* on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Licence for the specific language governing permissions and limitations under the Licence.
*/
import { NgClass } from '@angular/common';
import { AsyncPipe, NgClass } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, signal } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { Duration } from 'luxon';
import { Observable, Subject, interval, map, startWith, switchMap, take } from 'rxjs';

@Component({
selector: 'xm-examination-clock',
template: `<div class="floating-clock">
<div class="row">
<div class="header-wrapper col-12">
<div class="row align-items-center p-2">
@if (showRemainingTime) {
@if (showRemainingTime()) {
<div class="col-5">
<span class="sitnet-white">{{ 'i18n_exam_time_left' | translate }}: </span>
</div>
}
@if (!showRemainingTime) {
} @else {
<div class="col-5 clock-hide text-muted">
{{ 'i18n_clock_hidden' | translate }}
</div>
}
<div class="col-5">
@if (showRemainingTime) {
@if (showRemainingTime()) {
<span
class="exam-clock"
role="region"
[ngClass]="remainingTime <= alarmThreshold ? 'sitnet-text-alarm' : ''"
[attr.aria-live]="remainingTime <= alarmThreshold ? 'polite' : 'off'"
>{{ formatRemainingTime() }}</span
[ngClass]="(isTimeScarce$ | async) ? 'text-warning' : ''"
[attr.aria-live]="(isTimeScarce$ | async) ? 'polite' : 'off'"
>{{ remainingTime$ | async }}</span
>
}
</div>
<div class="col-2">
<button (click)="showRemainingTime = !showRemainingTime" class="border-none background-none">
<img
src="/assets/images/icon_clock.svg"
alt="{{ 'i18n_show_hide_clock' | translate }}"
onerror="this.onerror=null;this.src='/assets/images/icon_clock.png';"
/>
<button
(click)="showRemainingTime.set(!showRemainingTime())"
class="border-none background-none"
>
<img src="/assets/images/icon_clock.svg" alt="{{ 'i18n_show_hide_clock' | translate }}" />
</button>
</div>
</div>
</div>
</div>
</div>`,
standalone: true,
imports: [NgClass, TranslateModule],
imports: [NgClass, AsyncPipe, TranslateModule],
})
export class ExaminationClockComponent implements OnInit, OnDestroy {
@Input() examHash = '';
@Output() timedOut = new EventEmitter<void>();
syncInterval = 15;
secondsSinceSync = this.syncInterval + 1;
alarmThreshold = 300;
remainingTime = this.alarmThreshold + 1;
showRemainingTime = true;
pollerId = 0;

showRemainingTime = signal(false);
remainingTime$?: Observable<string>;
isTimeScarce$?: Observable<boolean>;

private syncInterval = 60;
private alarmThreshold = 300;
private subject = new Subject<number>();
private destroy = new Subject();

constructor(private http: HttpClient) {}

ngOnInit() {
this.checkRemainingTime();
const sync$ = this.http.get<number>(`/app/time/${this.examHash}`);
interval(this.syncInterval * 1000)
.pipe(
startWith(0),
switchMap(() =>
sync$.pipe(
switchMap((t) =>
interval(1000).pipe(
map((n) => t - n),
take(this.syncInterval),
),
),
),
),
)
.subscribe(this.subject);

this.remainingTime$ = this.subject.pipe(map((n) => Duration.fromObject({ seconds: n }).toFormat('hh:mm:ss')));
this.isTimeScarce$ = this.subject.pipe(map((n) => n <= this.alarmThreshold));
this.subject.subscribe((n) => {
if (n === 0) this.timedOut.emit();
});
this.showRemainingTime.set(true);
}

ngOnDestroy() {
if (this.pollerId) {
window.clearTimeout(this.pollerId);
}
this.destroy.next(undefined);
this.destroy.complete();
}

formatRemainingTime = (): string => {
if (!this.remainingTime) {
return '';
}
const hours = Math.floor(this.remainingTime / 60 / 60);
const minutes = Math.floor(this.remainingTime / 60) % 60;
const seconds = this.remainingTime % 60;
return `${hours}:${this.zeroPad(minutes)}:${this.zeroPad(seconds)}`;
};

private checkRemainingTime = () => {
this.secondsSinceSync++;
if (this.secondsSinceSync > this.syncInterval) {
// Sync time with backend
this.secondsSinceSync = 0;
this.setRemainingTime();
} else if (this.remainingTime !== undefined) {
// Decrease seconds
this.remainingTime--;
}
if (this.remainingTime !== undefined && this.remainingTime <= 0) {
this.notifyTimeout();
}

this.pollerId = window.setTimeout(this.checkRemainingTime, 1000);
};

private setRemainingTime = () =>
this.http.get<number>('/app/time/' + this.examHash).subscribe((resp) => (this.remainingTime = resp));

private notifyTimeout = () => {
window.clearTimeout(this.pollerId);
this.timedOut.emit();
};

private zeroPad = (n: number): string => ('0' + n).slice(-2);
}
26 changes: 0 additions & 26 deletions ui/src/app/examination/header/examination-header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,32 +45,6 @@ import type { Examination } from '../examination.model';
}
</div>
</div>
<div class="padr0 padl0 visible-mobile">
@if (!isPreview) {
<xm-examination-clock [examHash]="exam.hash" (timedOut)="notifyTimeout()"> </xm-examination-clock>
}
<div class="exam-mobile-header padt40">
<div class="row">
<h1 class="exam-header-title col marl20 marr20">
{{ exam.course?.name }}
@if (exam.course) {
<xm-course-code [course]="exam.course"></xm-course-code>
}
</h1>
</div>
<div class="exam-header-title mobile-divider row"></div>
<div class="row">
<div class="exam-header-img-wrap col">
<img src="/assets/images//exam-logo-mobile.svg" alt="" />
</div>
<div class="language-selector col">
<button class="green_button marl10" (click)="switchLanguage('fi')">FI</button>
<button class="green_button marl10" (click)="switchLanguage('sv')">SV</button>
<button class="green_button marl10" (click)="switchLanguage('en')">EN</button>
</div>
</div>
</div>
</div>
</div>`,
standalone: true,
imports: [CourseCodeComponent, ExaminationClockComponent],
Expand Down

0 comments on commit 852c885

Please sign in to comment.