Skip to content

Exam mode: Migrate participate module to signals #10456

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

Open
wants to merge 59 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
6283c32
migrate exam-bar.component
coolchock Mar 6, 2025
04002c5
migrate exam-timer.component
coolchock Mar 6, 2025
cad80ac
migrate text-exam-summary.component
coolchock Mar 6, 2025
3dbfef9
migrate quiz-exam-summary.component
coolchock Mar 6, 2025
d2f2efc
migrate programming-exam-summary.component
coolchock Mar 6, 2025
442a149
migrate exam-result-overview.component
coolchock Mar 6, 2025
7b9da2d
migrate modeling-exam-summary.component
coolchock Mar 6, 2025
db297ab
exam-result-summary-exercise-card-header.component
coolchock Mar 6, 2025
203be39
migate file-upload-exam-summary.component
coolchock Mar 6, 2025
714f6b6
migrate exam-start-information.component
coolchock Mar 6, 2025
0a051bf
migrate exam-general-information.component
coolchock Mar 6, 2025
8a4cf74
fix exam-result-summary-exercise-card-header.component.spec.ts
coolchock Mar 6, 2025
bee2026
fix exam-start-information.component.spec.ts
coolchock Mar 6, 2025
277edeb
fix exam-general-information.component.spec.ts
coolchock Mar 6, 2025
8431840
fix quiz-exam-summary.component.spec.ts
coolchock Mar 6, 2025
78f6cc7
fix programming-exam-summary.component.spec.ts
coolchock Mar 7, 2025
5ba33b0
fix exam-result-overview.component.spec.ts
coolchock Mar 7, 2025
08a6b7c
fix file-upload-exam-summary.component.spec.ts
coolchock Mar 7, 2025
62daa58
fix modeling-exam-summary.component.spec.ts
coolchock Mar 7, 2025
402136d
migrate exam-participation.component.ts
coolchock Mar 9, 2025
47a5ef6
migrate exam-navigation-bar.component
coolchock Mar 9, 2025
cd3983d
migrate exam-navigation-bar.component
coolchock Mar 9, 2025
605f5c0
set default value
coolchock Mar 9, 2025
21f9921
migrate exam-participation-cover.component
coolchock Mar 9, 2025
1025b43
migrate collapsible-card.component
coolchock Mar 9, 2025
c288d30
migrate overlay and button for live events
coolchock Mar 9, 2025
a2ddf9b
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Mar 9, 2025
6b5b5a3
fix issue with model
coolchock Mar 9, 2025
7f22d72
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Mar 17, 2025
d71cb6a
resolve conflict
coolchock Mar 17, 2025
6b4f4a1
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Mar 25, 2025
a4bdb44
resolve conflicts
coolchock Mar 25, 2025
4133b90
remove redundant brackets
coolchock Mar 25, 2025
8f1743d
fix exam-start-information.component.spec.ts
coolchock Mar 25, 2025
11c552f
fix tests
coolchock Mar 25, 2025
4b75955
migrate last input
coolchock Mar 25, 2025
45eef41
fix exam-result-summary.component.spec.ts
coolchock Mar 25, 2025
a54af0b
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Mar 25, 2025
82e8515
fix exam-result-summary.component.spec.ts
coolchock Mar 26, 2025
5d19016
remove redundant comment
coolchock Mar 26, 2025
51b3d19
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Mar 26, 2025
1caf9db
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Mar 27, 2025
0d45d73
resolve conflicts
coolchock Mar 27, 2025
3aea879
fix imports
coolchock Mar 27, 2025
ea6fef1
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Mar 30, 2025
e6942dd
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Apr 21, 2025
cbe482e
resolve conflict
coolchock Apr 21, 2025
7697664
fix programming-exam-summary.component.spec.ts
coolchock Apr 22, 2025
a190dcf
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Apr 22, 2025
64864be
Merge branch 'develop' into chore/exam-mode/migrate-participate-modul…
coolchock Apr 22, 2025
5cbd213
move inputs setup to beforeEach
coolchock Apr 25, 2025
10fe5e8
remove commented code
coolchock Apr 25, 2025
5171eb3
use set instead of update
coolchock Apr 25, 2025
4589cf6
remove redundant element
coolchock Apr 26, 2025
d693002
make input parameter required
coolchock Apr 26, 2025
52baf3b
make modal required
coolchock Apr 26, 2025
40bbfdf
remove unused import
coolchock Apr 26, 2025
b80c1d2
move input setup to beforeEach
coolchock Apr 26, 2025
5ec982c
remove uncommented code
coolchock Apr 28, 2025
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
Expand Up @@ -6,6 +6,8 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ExamLiveEvent, ExamLiveEventType, ExamParticipationLiveEventsService } from 'app/exam/overview/services/exam-participation-live-events.service';
import { of } from 'rxjs';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { input } from '@angular/core';
import dayjs from 'dayjs/esm';
import { MockExamParticipationLiveEventsService } from 'test/helpers/mocks/service/mock-exam-participation-live-events.service';
import { ExamLiveEventsOverlayComponent } from 'app/exam/overview/events/overlay/exam-live-events-overlay.component';

Expand All @@ -21,13 +23,13 @@ describe('ExamLiveEventsButtonComponent', () => {
imports: [MockModule(FontAwesomeModule)],
providers: [MockProvider(AlertService), MockProvider(NgbModal), { provide: ExamParticipationLiveEventsService, useClass: MockExamParticipationLiveEventsService }],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ExamLiveEventsButtonComponent);
component = fixture.componentInstance;
mockModalService = TestBed.inject(NgbModal);
mockLiveEventsService = TestBed.inject(ExamParticipationLiveEventsService);
TestBed.runInInjectionContext(() => {
component.examStartDate = input(dayjs());
});
fixture.detectChanges();
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, OnDestroy, OnInit, ViewEncapsulation, inject } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewEncapsulation, inject, input } from '@angular/core';
import { faBullhorn } from '@fortawesome/free-solid-svg-icons';
import { AlertService } from 'app/shared/service/alert.service';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
Expand Down Expand Up @@ -32,19 +32,19 @@ export class ExamLiveEventsButtonComponent implements OnInit, OnDestroy {
private liveEventsSubscription?: Subscription;
private allEventsSubscription?: Subscription;
eventCount = 0;
@Input() examStartDate: dayjs.Dayjs;
examStartDate = input.required<dayjs.Dayjs>();

// Icons
faBullhorn = faBullhorn;

ngOnInit(): void {
this.allEventsSubscription = this.liveEventsService.observeAllEvents(USER_DISPLAY_RELEVANT_EVENTS_REOPEN).subscribe((events: ExamLiveEvent[]) => {
// do not count the problem statements events that are made before the start of the exam
const filteredEvents = events.filter((event) => !(event.eventType === ExamLiveEventType.PROBLEM_STATEMENT_UPDATE && event.createdDate.isBefore(this.examStartDate)));
const filteredEvents = events.filter((event) => !(event.eventType === ExamLiveEventType.PROBLEM_STATEMENT_UPDATE && event.createdDate.isBefore(this.examStartDate())));
this.eventCount = filteredEvents.length;
});

this.liveEventsSubscription = this.liveEventsService.observeNewEventsAsUser(USER_DISPLAY_RELEVANT_EVENTS, this.examStartDate).subscribe(() => {
this.liveEventsSubscription = this.liveEventsService.observeNewEventsAsUser(USER_DISPLAY_RELEVANT_EVENTS, this.examStartDate()).subscribe(() => {
// If any unacknowledged event comes in, open the dialog to display it
if (!this.modalRef) {
this.openDialog();
Expand All @@ -69,7 +69,7 @@ export class ExamLiveEventsButtonComponent implements OnInit, OnDestroy {
windowClass: 'live-events-modal-window',
});

this.modalRef.componentInstance.examStartDate = this.examStartDate;
this.modalRef.componentInstance.examStartDate.set(this.examStartDate());

from(this.modalRef.result).subscribe(() => (this.modalRef = undefined));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { MockSyncStorage } from 'test/helpers/mocks/service/mock-sync-storage.se
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { MockProvider } from 'ng-mocks';
import dayjs from 'dayjs/esm';
import { model } from '@angular/core';

describe('ExamLiveEventsOverlayComponent', () => {
let component: ExamLiveEventsOverlayComponent;
Expand All @@ -28,14 +30,15 @@ describe('ExamLiveEventsOverlayComponent', () => {
MockProvider(NgbActiveModal),
],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ExamLiveEventsOverlayComponent);
component = fixture.componentInstance;
mockLiveEventsService = TestBed.inject(ExamParticipationLiveEventsService);
mockExamExerciseUpdateService = TestBed.inject(ExamExerciseUpdateService);
mockActiveModal = TestBed.inject(NgbActiveModal);
TestBed.runInInjectionContext(() => {
component.examStartDate = model(dayjs());
});
fixture.detectChanges();
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, OnDestroy, OnInit, inject } from '@angular/core';
import { Component, OnDestroy, OnInit, inject, model } from '@angular/core';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { ExamLiveEventComponent } from 'app/exam/shared/events/exam-live-event.component';
import { Subscription } from 'rxjs';
Expand Down Expand Up @@ -33,7 +33,7 @@ export class ExamLiveEventsOverlayComponent implements OnInit, OnDestroy {
eventsToDisplay?: ExamLiveEvent[];
events: ExamLiveEvent[] = [];

@Input() examStartDate: dayjs.Dayjs;
examStartDate = model.required<dayjs.Dayjs>();
// Icons
faCheck = faCheck;

Expand All @@ -47,13 +47,13 @@ export class ExamLiveEventsOverlayComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.allLiveEventsSubscription = this.liveEventsService.observeAllEvents(USER_DISPLAY_RELEVANT_EVENTS_REOPEN).subscribe((events: ExamLiveEvent[]) => {
// display the problem statements events only after the start of the exam
this.events = events.filter((event) => !(event.eventType === ExamLiveEventType.PROBLEM_STATEMENT_UPDATE && event.createdDate.isBefore(this.examStartDate)));
this.events = events.filter((event) => !(event.eventType === ExamLiveEventType.PROBLEM_STATEMENT_UPDATE && event.createdDate.isBefore(this.examStartDate())));
if (!this.eventsToDisplay) {
this.updateEventsToDisplay();
}
});

this.newLiveEventsSubscription = this.liveEventsService.observeNewEventsAsUser(USER_DISPLAY_RELEVANT_EVENTS, this.examStartDate).subscribe((event: ExamLiveEvent) => {
this.newLiveEventsSubscription = this.liveEventsService.observeNewEventsAsUser(USER_DISPLAY_RELEVANT_EVENTS, this.examStartDate()).subscribe((event: ExamLiveEvent) => {
this.unacknowledgedEvents.unshift(event);
this.updateEventsToDisplay();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<div class="h5 module-bg exam-bar mt-0 px-3 py-2 rounded rounded-3 sticky-top" [ngClass]="{ 'end-view': isEndView, 'mx-3': !isEndView }">
<div class="h5 module-bg exam-bar mt-0 px-3 py-2 rounded rounded-3 sticky-top" [ngClass]="{ 'end-view': isEndView(), 'mx-3': !isEndView() }">
<div class="d-flex justify-content-between">
<h3 class="align-self-center mb-0 me-3">
{{ examTitle }}
</h3>
@if (!examTimeLineView) {
@if (!examTimeLineView()) {
<div class="d-flex justify-content-between align-items-center">
<jhi-exam-timer
class="me-3"
[criticalTime]="isEndView ? criticalTimeEndView : criticalTime"
[endDate]="endDate"
[isEndView]="isEndView"
[criticalTime]="isEndView() ? criticalTimeEndView : criticalTime"
[endDate]="endDate()"
[isEndView]="isEndView()"
(timerAboutToEnd)="triggerExamAboutToEnd()"
/>
<jhi-exam-live-events-button [examStartDate]="examStartDate" />
@if (!isEndView) {
<jhi-exam-live-events-button [examStartDate]="examStartDate()" />
@if (!isEndView()) {
<button id="hand-in-early" class="btn btn-danger ms-2" aria-label="Hand In Early" (click)="handInEarly()">
<div class="d-flex justify-content-between">
<span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AlertService } from 'app/shared/service/alert.service';
import { MockTranslateService } from 'test/helpers/mocks/service/mock-translate.service';
import { TranslateService } from '@ngx-translate/core';
import { provideHttpClient } from '@angular/common/http';
import { input } from '@angular/core';

describe('ExamBarComponent', () => {
let fixture: ComponentFixture<ExamBarComponent>;
Expand All @@ -36,11 +37,6 @@ describe('ExamBarComponent', () => {

fixture = TestBed.createComponent(ExamBarComponent);
comp = fixture.componentInstance;

comp.exam = new Exam();
comp.exam.title = 'Test Exam';
comp.studentExam = new StudentExam();
comp.endDate = dayjs();
const exercises = [
{
id: 0,
Expand All @@ -54,7 +50,16 @@ describe('ExamBarComponent', () => {
{ id: 1, type: ExerciseType.TEXT } as Exercise,
{ id: 2, type: ExerciseType.MODELING } as Exercise,
];
comp.studentExam.exercises = exercises;

TestBed.runInInjectionContext(() => {
comp.exam = input(new Exam());
comp.exam().title = 'Test Exam';
comp.studentExam = input(new StudentExam());
comp.endDate = input(dayjs());
comp.examStartDate = input(dayjs());
comp.studentExam().exercises = exercises;
comp.isEndView = input(false);
});
});

beforeEach(() => {
Expand Down
36 changes: 18 additions & 18 deletions src/main/webapp/app/exam/overview/exam-bar/exam-bar.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnInit, Output, inject } from '@angular/core';
import { AfterViewInit, Component, ElementRef, OnInit, inject, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ExamParticipationService } from 'app/exam/overview/services/exam-participation.service';
Expand All @@ -21,18 +21,18 @@ import { TranslateDirective } from 'app/shared/language/translate.directive';
export class ExamBarComponent implements AfterViewInit, OnInit {
private elementRef = inject(ElementRef);

@Output() onExamHandInEarly = new EventEmitter<void>();
@Output() examAboutToEnd = new EventEmitter<void>();
@Output() heightChange = new EventEmitter<number>();
onExamHandInEarly = output<void>();
examAboutToEnd = output<void>();
heightChange = output<number>();

@Input() examTimeLineView = false;
@Input() endDate: dayjs.Dayjs;
@Input() exerciseIndex = 0;
@Input() isEndView: boolean;
@Input() testRunStartTime: dayjs.Dayjs | undefined;
@Input() exam: Exam;
@Input() studentExam: StudentExam;
@Input() examStartDate: dayjs.Dayjs;
examTimeLineView = input(false);
endDate = input.required<dayjs.Dayjs>();
exerciseIndex = input(0);
isEndView = input.required<boolean>();
testRunStartTime = input<dayjs.Dayjs>();
exam = input.required<Exam>();
studentExam = input.required<StudentExam>();
examStartDate = input.required<dayjs.Dayjs>();

readonly faDoorClosed = faDoorClosed;
criticalTime = dayjs.duration(5, 'minutes');
Expand All @@ -45,10 +45,10 @@ export class ExamBarComponent implements AfterViewInit, OnInit {
exercises: Exercise[] = [];

ngOnInit(): void {
this.examTitle = this.exam.title ?? '';
this.exercises = this.studentExam.exercises ?? [];
this.testExam = this.exam.testExam ?? false;
this.testRun = this.studentExam.testRun ?? false;
this.examTitle = this.exam().title ?? '';
this.exercises = this.studentExam().exercises ?? [];
this.testExam = this.exam().testExam ?? false;
this.testRun = this.studentExam().testRun ?? false;
}

/**
Expand Down Expand Up @@ -77,9 +77,9 @@ export class ExamBarComponent implements AfterViewInit, OnInit {
* Save the currently active exercise
*/
saveExercise() {
const submission = ExamParticipationService.getSubmissionForExercise(this.exercises[this.exerciseIndex]);
const submission = ExamParticipationService.getSubmissionForExercise(this.exercises[this.exerciseIndex()]);
// we do not submit programming exercises on a save
if (submission && this.exercises[this.exerciseIndex].type !== ExerciseType.PROGRAMMING) {
if (submission && this.exercises[this.exerciseIndex()].type !== ExerciseType.PROGRAMMING) {
submission.submitted = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
-->
<div
class="px-3 pb-3 overflow-auto"
[ngClass]="{ 'content-height-dev': (!isProduction || isTestServer) && !testRunStartTime, 'scrollable-content-exam-cover': !testRunStartTime && startView }"
[ngClass]="{ 'content-height-dev': (!isProduction() || isTestServer()) && !testRunStartTime(), 'scrollable-content-exam-cover': !testRunStartTime() && startView() }"
>
@if (startView) {
@if (startView()) {
<div class="d-flex justify-content-between">
<h3 class="mt-3">
{{ exam.title }}
{{ exam().title }}
</h3>
<div class="mt-3">
<jhi-exam-live-events-button [examStartDate]="exam.startDate!" />
<jhi-exam-live-events-button [examStartDate]="exam().startDate!" />
</div>
</div>
<hr class="my-0" />
<div class="mt-3">
<jhi-exam-start-information [exam]="exam" [studentExam]="studentExam" [formattedStartText]="formattedGeneralInformation" />
<jhi-exam-start-information [exam]="exam()" [studentExam]="studentExam()" [formattedStartText]="formattedGeneralInformation" />
</div>
<div class="d-inline-flex align-items-center my-3">
<div class="ps-1">
Expand All @@ -26,7 +26,7 @@ <h3 class="mt-3">
id="confirmBox"
(click)="updateConfirmation()"
class="form-check-input me-2"
[class.ms-0]="!this.exam.confirmationStartText"
[class.ms-0]="!this.exam().confirmationStartText"
[required]="inserted"
[disabled]="waitingForExamStart"
/>
Expand Down Expand Up @@ -90,14 +90,14 @@ <h3 class="mt-3">
</ng-container>
@if (waitingForExamStart) {
<div class="exam-waiting-for-start-overlay alert alert-info">
<span jhiTranslate="artemisApp.examParticipation.waitForStart" [translateValues]="{ title: exam.title }"></span>
@if (exam.startDate) {
<span jhiTranslate="artemisApp.examParticipation.waitForStart" [translateValues]="{ title: exam().title }"></span>
@if (exam().startDate) {
<div>
<hr />
<span jhiTranslate="artemisApp.examParticipation.timeUntilPlannedStart"></span>
<span class="text-bold">{{ timeUntilStart }}</span>
<br />
<span>({{ exam.startDate | artemisDate: 'time' }})</span>
<span>({{ exam().startDate | artemisDate: 'time' }})</span>
</div>
}
</div>
Expand All @@ -108,7 +108,7 @@ <h4 id="exam-finished-title">
<span
jhiTranslate="artemisApp.examParticipation.finish"
[translateValues]="{
title: exam.title,
title: exam().title,
}"
></span>
</h4>
Expand All @@ -119,7 +119,7 @@ <h4 id="exam-finished-title">
<div class="mb-1">
<span class="fw-bold" jhiTranslate="artemisApp.examParticipation.submitFinalExam"></span>
</div>
@if (handInEarly) {
@if (handInEarly()) {
<div class="mb-3">
<div class="mb-1 mt-3 fw-bold text-danger">
<fa-icon [icon]="faCircleExclamation" />
Expand All @@ -143,7 +143,7 @@ <h4 id="exam-finished-title">
id="confirmBox"
(click)="updateConfirmation()"
class="form-check-input me-2"
[class.ms-0]="!this.exam.confirmationEndText"
[class.ms-0]="!this.exam().confirmationEndText"
[required]="inserted"
/>
<label for="confirmBox" id="formatted-confirmation-text" class="form-check-label" [innerHTML]="formattedConfirmationText"></label>
Expand Down Expand Up @@ -193,20 +193,20 @@ <h4 id="exam-finished-title">
</div>
</div>
</div>
@if (handInEarly) {
@if (handInEarly()) {
<div class="mt-3">
<div class="mb-2 font-weight-bold text-secondary" jhiTranslate="artemisApp.examParticipation.continueAfterHandInEarlyDescription"></div>
</div>
}
<div class="d-flex justify-content-end gap-3">
@if (handInEarly) {
<button [disabled]="submitInProgress" id="continue" class="btn btn-secondary" (click)="continueAfterHandInEarly()">
@if (handInEarly()) {
<button [disabled]="submitInProgress()" id="continue" class="btn btn-secondary" (click)="continueAfterHandInEarly()">
<fa-icon [icon]="faArrowLeft" />
<span jhiTranslate="artemisApp.examParticipation.continueAfterHandInEarly"></span>
</button>
}
<button id="end-exam" [disabled]="!endButtonEnabled" type="submit" (click)="submitExam()" class="btn btn-primary">
@if (submitInProgress) {
@if (submitInProgress()) {
<fa-icon class="me-1" [icon]="faSpinner" animation="spin" />
} @else {
<fa-icon class="me-1" [icon]="faDoorClosed" />
Expand Down
Loading
Loading