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 authored and lupari committed Jan 25, 2024
1 parent bd4ecbf commit 6ea6627
Show file tree
Hide file tree
Showing 14 changed files with 86 additions and 28 deletions.
1 change: 1 addition & 0 deletions app/controllers/EnrolmentController.java
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ private static ExamEnrolment makeEnrolment(Exam exam, User user) {
enrolment.setUser(user);
}
enrolment.setExam(exam);
enrolment.setRandomDelay();
enrolment.save();
return enrolment;
}
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/TimeController.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public Result getRemainingExamTime(String hash, Http.Request request) throws IOE
return notFound();
}

DateTime reservationStart = getStart(enrolment);
DateTime reservationStart = getStart(enrolment).plusSeconds(enrolment.getDelay());
int durationMinutes = getDuration(enrolment);
DateTime now = getNow(enrolment);
Seconds timeLeft = Seconds.secondsBetween(now, reservationStart.plusMinutes(durationMinutes));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ private static ExamEnrolment makeEnrolment(CollaborativeExam exam, User user) {
enrolment.setEnrolledOn(DateTime.now());
enrolment.setUser(user);
enrolment.setCollaborativeExam(exam);
enrolment.setRandomDelay();
enrolment.save();
return enrolment;
}
Expand All @@ -155,7 +156,7 @@ private Optional<Result> handleFutureReservations(List<ExamEnrolment> enrolments
}
// reservation in the future, replace it
if (!enrolmentsWithFutureReservations.isEmpty()) {
enrolmentsWithFutureReservations.get(0).delete();
enrolmentsWithFutureReservations.getFirst().delete();
ExamEnrolment newEnrolment = makeEnrolment(ce, user);
return Optional.of(ok(newEnrolment));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ public CompletionStage<ExamEnrolment> requestEnrolment(User user, Reservation re
enrolment.setExternalExam(ee);
enrolment.setReservation(reservation);
enrolment.setUser(user);
enrolment.setRandomDelay();
enrolment.save();
return enrolment;
};
Expand Down
17 changes: 17 additions & 0 deletions app/models/ExamEnrolment.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import jakarta.persistence.OneToOne;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import java.util.Random;
import java.util.Set;
import javax.annotation.Nonnull;
import models.base.GeneratedIdentityModel;
Expand All @@ -41,6 +42,8 @@
@Entity
public class ExamEnrolment extends GeneratedIdentityModel implements Comparable<ExamEnrolment> {

private static final int DELAY_MAX = 30;

@ManyToOne
@JsonManagedReference
private User user;
Expand Down Expand Up @@ -85,6 +88,8 @@ public class ExamEnrolment extends GeneratedIdentityModel implements Comparable<

private boolean retrialPermitted;

private int delay;

public User getUser() {
return user;
}
Expand Down Expand Up @@ -189,10 +194,22 @@ public void setRetrialPermitted(boolean retrialPermitted) {
this.retrialPermitted = retrialPermitted;
}

public int getDelay() {
return delay;
}

public void setDelay(int delay) {
this.delay = delay;
}

public boolean isProcessed() {
return (exam != null && exam.hasState(Exam.State.GRADED_LOGGED, Exam.State.ARCHIVED, Exam.State.DELETED));
}

public void setRandomDelay() {
this.setDelay(new Random().nextInt(DELAY_MAX));
}

@Override
public int compareTo(@Nonnull ExamEnrolment other) {
if (reservation == null && other.reservation == null) {
Expand Down
13 changes: 9 additions & 4 deletions app/repository/EnrolmentRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -319,20 +319,25 @@ private boolean isInsideBounds(ExamEnrolment ee, int minutesToFuture) {
ExaminationEvent event = ee.getExaminationEventConfiguration() != null
? ee.getExaminationEventConfiguration().getExaminationEvent()
: null;
int delay = ee.getDelay();
return (
(reservation != null &&
reservation.getStartAt().isBefore(latest) &&
reservation.getStartAt().plusSeconds(delay).isBefore(latest) &&
reservation.getEndAt().isAfter(earliest)) ||
(event != null &&
event.getStart().isBefore(latest) &&
event.getStart().plusSeconds(delay).isBefore(latest) &&
event.getStart().plusMinutes(ee.getExam().getDuration()).isAfter(earliest))
);
}

private DateTime getStartTime(ExamEnrolment enrolment) {
return enrolment.getReservation() != null
? enrolment.getReservation().getStartAt()
: enrolment.getExaminationEventConfiguration().getExaminationEvent().getStart();
? enrolment.getReservation().getStartAt().plusSeconds(enrolment.getDelay())
: enrolment
.getExaminationEventConfiguration()
.getExaminationEvent()
.getStart()
.plusSeconds(enrolment.getDelay());
}

private Optional<ExamEnrolment> getNextEnrolment(Long userId, int minutesToFuture) {
Expand Down
5 changes: 5 additions & 0 deletions conf/evolutions/default/133.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# --- !Ups
ALTER TABLE exam_enrolment ADD delay INT NOT NULL DEFAULT 0;

# --- !Downs
ALTER TABLE exam_enrolment DROP delay;
1 change: 1 addition & 0 deletions ui/src/app/enrolment/enrolment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface ExamEnrolment {
noShow: boolean;
retrialPermitted: boolean;
optionalSections: ExamSection[];
delay: number;
}

export interface EnrolmentInfo extends Exam {
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
23 changes: 14 additions & 9 deletions ui/src/app/enrolment/waiting-room/waiting-room.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ <h1 [hidden]="isUpcoming" class="student-enroll-title">
</div>
</div>
<div class="waitingroom-info">
<div [hidden]="!isUpcoming">
<img class="arrow_icon" alt="" role="note" src="/assets/images/icon_info.png" />
{{ 'i18n_redirect_to_exam_notice' | translate }}
</div>
@if (delayCounter$) {
<div class="alert alert-info me-4">
<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 me-4"></i>
{{ 'i18n_redirect_to_exam_notice' | translate }}
</div>
}
</div>

@if (isUpcoming) {
@if (isUpcoming()) {
<div class="row student-enrolment-wrapper waitingroom marl20" id="dashboard">
<div class="col-md-12">
<div class="row">
Expand Down Expand Up @@ -104,16 +111,14 @@ <h1 [hidden]="isUpcoming" class="student-enroll-title">
</div>
</div>
}
@if (roomInstructions) {
@if (roomInstructions()) {
<div class="row">
<div class="col-md-12 student-exam-row-infolink">
<strong>{{ 'i18n_room_guidance' | translate }}: </strong>
</div>
</div>
}
@if (roomInstructions) {
<div class="row">
<div class="col-md-12 student-exam-row-infobox">{{ roomInstructions }}</div>
<div class="col-md-12 student-exam-row-infobox">{{ roomInstructions() }}</div>
</div>
}
@if (enrolment?.exam?.enrollInstruction) {
Expand Down
37 changes: 28 additions & 9 deletions ui/src/app/enrolment/waiting-room/waiting-room.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
* 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 { DatePipe, SlicePipe, UpperCasePipe } from '@angular/common';
import { AsyncPipe, DatePipe, SlicePipe, UpperCasePipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import type { OnDestroy, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { Component, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { DateTime } from 'luxon';
Expand All @@ -28,6 +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 } from 'rxjs';

type WaitingReservation = Reservation & { occasion: { startAt: string; endAt: string } };
type WaitingEnrolment = Omit<ExamEnrolment, 'reservation'> & {
Expand All @@ -42,6 +43,7 @@ type WaitingEnrolment = Omit<ExamEnrolment, 'reservation'> & {
CourseCodeComponent,
TeacherListComponent,
MathJaxDirective,
AsyncPipe,
UpperCasePipe,
SlicePipe,
DatePipe,
Expand All @@ -51,9 +53,12 @@ type WaitingEnrolment = Omit<ExamEnrolment, 'reservation'> & {
})
export class WaitingRoomComponent implements OnInit, OnDestroy {
enrolment!: WaitingEnrolment;
isUpcoming = false;
timeoutId = 0;
roomInstructions = '';
isUpcoming = signal(false);
roomInstructions = signal('');
delayCounter$?: Observable<number>;

private startTimerId = 0;
private delayTimerId = 0;

constructor(
private http: HttpClient,
Expand All @@ -66,17 +71,17 @@ export class WaitingRoomComponent implements OnInit, OnDestroy {

ngOnInit() {
if (this.route.snapshot.params.id && this.route.snapshot.params.hash) {
this.isUpcoming = true;
this.isUpcoming.set(true);
this.http.get<WaitingEnrolment>(`/app/student/enrolments/${this.route.snapshot.params.id}`).subscribe({
next: (enrolment) => {
this.setOccasion(enrolment.reservation);
this.enrolment = enrolment;
const offset = this.calculateOffset();
this.timeoutId = window.setTimeout(this.Session.checkSession, offset);
this.startTimerId = window.setTimeout(this.startScheduled, offset);
if (this.enrolment.reservation) {
const room = this.enrolment.reservation.machine.room;
const code = this.translate.currentLang.toUpperCase();
this.roomInstructions = this.getRoomInstructions(code, room);
this.roomInstructions.set(this.getRoomInstructions(code, room));
}
this.http
.post<void>(`/app/student/exam/${this.route.snapshot.params.hash}`, {})
Expand All @@ -88,9 +93,23 @@ export class WaitingRoomComponent implements OnInit, OnDestroy {
}

ngOnDestroy() {
window.clearTimeout(this.timeoutId);
window.clearTimeout(this.startTimerId);
window.clearTimeout(this.delayTimerId);
}

private startScheduled = () => {
window.clearTimeout(this.startTimerId);
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 * 1000);
this.delayCounter$ = interval(1000).pipe(
startWith(0),
map((n) => offset - n),
);
};

private getRoomInstructions = (lang: string, room: ExamRoom) => {
switch (lang) {
case 'FI':
Expand Down
3 changes: 2 additions & 1 deletion ui/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1159,5 +1159,6 @@
"i18n_button_preview": "Preview question",
"i18n_no_preview_available": "No preview available",
"i18n_used_in_exams": "Used in exams",
"i18n_quit_password": "SEB-poistumissalasana EN"
"i18n_quit_password": "SEB-poistumissalasana EN",
"i18n_seconds_until_examination_starts": "sekuntia tentin käynnistymiseen EN"
}
3 changes: 2 additions & 1 deletion ui/src/assets/i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1159,5 +1159,6 @@
"i18n_button_preview": "Esikatsele kysymys",
"i18n_no_preview_available": "Kysymyksen esikatselu ei ole saatavilla",
"i18n_used_in_exams": "Käytössä tenteissä",
"i18n_quit_password": "SEB-poistumissalasana"
"i18n_quit_password": "SEB-poistumissalasana",
"i18n_seconds_until_examination_starts": "sekuntia tentin käynnistymiseen"
}
3 changes: 2 additions & 1 deletion ui/src/assets/i18n/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -1159,5 +1159,6 @@
"i18n_button_preview": "Preview question SV",
"i18n_no_preview_available": "No preview available SV",
"i18n_used_in_exams": "Käytössä tenteissä SV",
"i18n_quit_password": "SEB-poistumissalasana SV"
"i18n_quit_password": "SEB-poistumissalasana SV",
"i18n_seconds_until_examination_starts": "sekuntia tentin käynnistymiseen SV"
}

0 comments on commit 6ea6627

Please sign in to comment.