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

Development: Migrate exam exercises client code to signals #10329

Merged
merged 37 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
02a2a4a
migrate exam-exercise-update-highlighter.component
coolchock Dec 1, 2024
2941472
migrate exam-exercise-overview-page.component.ts
coolchock Dec 1, 2024
621c0b5
migrate exam-exercise-update-highlighter.module.ts
coolchock Dec 1, 2024
35fb09f
migrate exam-page.component.ts
coolchock Dec 1, 2024
cec5168
migrate exam-submission.component.ts
coolchock Dec 1, 2024
1e9dafa
migrate exam-submission-components.component.ts
coolchock Dec 1, 2024
1244188
migrate file-upload-exam-submission.component
coolchock Dec 1, 2024
c495c3a
migrate modeling-exam-submission.component
coolchock Dec 1, 2024
6fac3b1
migrate programming-exam-submission.component
coolchock Dec 1, 2024
7c3a6e6
migrate quiz-exam-submission.component
coolchock Dec 1, 2024
8354a59
migrate text-exam-submission.component
coolchock Dec 1, 2024
98cffb2
fix errors
coolchock Dec 1, 2024
7d74856
fix input assignment
coolchock Dec 1, 2024
c59bd8f
fix text-exam-submission.component.spec.ts
coolchock Dec 1, 2024
f5696f0
fix quiz-exam-submission.component.spec.ts
coolchock Dec 1, 2024
ed3c10b
fix programming-exam-submission.component.spec.ts
coolchock Dec 1, 2024
1d95bf8
fix exam-exercise-overview-page.component.spec.ts
coolchock Dec 1, 2024
a493333
fix exam-exercise-update-highlighter.component.spec.ts
coolchock Dec 1, 2024
a73dcca
fix file-upload-exam-submission.component
coolchock Dec 1, 2024
1f64060
fix modeling-exam-submission.component.spec.ts
coolchock Dec 1, 2024
0dacd79
use model instead of input
coolchock Dec 2, 2024
ddd1199
fix file and modeling exercise tests
coolchock Dec 2, 2024
a6502b2
Merge branch 'develop' into chore/exam-exercises-client-migration
coolchock Jan 4, 2025
52595e5
remove redundant module
coolchock Jan 4, 2025
f88298d
remove redundant exam submission module
coolchock Jan 4, 2025
75b797e
fix student-exam-timeline tests
coolchock Jan 4, 2025
fa2423d
fix exam-participation.component.spec.ts
coolchock Jan 4, 2025
069178c
fix exam-exercise-update-highlighter.component.spec.ts
coolchock Jan 4, 2025
d1ff991
fix tests
coolchock Jan 4, 2025
c02c4e3
optimize tests
coolchock Jan 4, 2025
36a07be
remove duplicated reset
coolchock Jan 4, 2025
53c048a
Merge branch 'develop' into chore/exam-exercises-client-migration
coolchock Feb 12, 2025
a141025
fix tests
coolchock Feb 12, 2025
5d5507d
Merge branch 'develop' into chore/exam-exercises-client-migration
coolchock Feb 13, 2025
f825009
Merge branch 'develop' into chore/exam-exercises-client-migration
coolchock Feb 14, 2025
12e658a
remove unused change detector
coolchock Feb 16, 2025
70d0bbc
Merge branch 'develop' into chore/exam-exercises-client-migration
coolchock Feb 16, 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 @@ -160,7 +160,7 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit, OnDe
private updateFileUploadExerciseView() {
const fileUploadComponent = this.activePageComponent as FileUploadExamSubmissionComponent;
if (fileUploadComponent) {
fileUploadComponent.studentSubmission = this.currentSubmission as FileUploadSubmission;
fileUploadComponent.studentSubmission.update(() => this.currentSubmission as FileUploadSubmission);
fileUploadComponent.updateViewFromSubmission();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, inject } from '@angular/core';
import { Component, OnDestroy, OnInit, inject, input, output } from '@angular/core';
import { Subscription } from 'rxjs';
import { ExamExerciseUpdateService } from 'app/exam/manage/exam-exercise-update.service';
import { Exercise, ExerciseType } from 'app/entities/exercise.model';
Expand All @@ -23,9 +23,9 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit, OnDestroy
updatedProblemStatement: string;
showHighlightedDifferences = true;
isHidden = true;
@Input() exercise: Exercise;
exercise = input.required<Exercise>();

@Output() problemStatementUpdateEvent: EventEmitter<string> = new EventEmitter<string>();
problemStatementUpdateEvent = output<string>();

ngOnInit(): void {
this.subscriptionToLiveExamExerciseUpdates = this.examExerciseUpdateService.currentExerciseIdAndProblemStatement.subscribe((update) => {
Expand Down Expand Up @@ -64,14 +64,14 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit, OnDestroy
* @param updatedProblemStatement is the new problem statement that should replace the old one.
*/
updateExerciseProblemStatementById(exerciseId: number, updatedProblemStatement: string) {
if (updatedProblemStatement != undefined && exerciseId === this.exercise.id) {
this.outdatedProblemStatement = this.exercise.problemStatement!;
if (updatedProblemStatement != undefined && exerciseId === this.exercise().id) {
this.outdatedProblemStatement = this.exercise().problemStatement!;
this.updatedProblemStatement = updatedProblemStatement;
this.exercise.problemStatement = updatedProblemStatement;
this.exercise().problemStatement = updatedProblemStatement;
this.showHighlightedDifferences = true;
// Highlighting of the changes in the problem statement of a programming exercise id handled
// in ProgrammingExerciseInstructionComponent
if (this.exercise.type !== ExerciseType.PROGRAMMING) {
if (this.exercise().type !== ExerciseType.PROGRAMMING) {
this.highlightProblemStatementDifferences();
}
this.isHidden = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Submission } from 'app/entities/submission.model';
import { Exercise, ExerciseType } from 'app/entities/exercise.model';
import { ExamPageComponent } from 'app/exam/participate/exercises/exam-page.component';
import { Directive, Input } from '@angular/core';
import { Directive, input } from '@angular/core';
import { SubmissionVersion } from 'app/entities/submission-version.model';

@Directive()
Expand Down Expand Up @@ -29,8 +29,8 @@ export abstract class ExamSubmissionComponent extends ExamPageComponent {
abstract getSubmission(): Submission | undefined;
abstract getExerciseId(): number | undefined;
abstract getExercise(): Exercise;
@Input() readonly = false;
@Input() examTimeline = false;
readonly = input(false);
examTimeline = input(false);
// needs to be public so that it can be accessed in the tests
submissionVersion: SubmissionVersion;
abstract setSubmissionVersion(submissionVersion: SubmissionVersion): void;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, inject } from '@angular/core';
import { ChangeDetectorRef, Component, OnChanges, OnInit, inject, input, output } from '@angular/core';
import { Exercise, ExerciseType, getIcon, getIconTooltip } from 'app/entities/exercise.model';
import { ExamPageComponent } from 'app/exam/participate/exercises/exam-page.component';
import { StudentExam } from 'app/entities/student-exam.model';
Expand All @@ -21,18 +21,23 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
imports: [TranslateDirective, FaIconComponent, NgbTooltip, NgClass, UpdatingResultComponent, ArtemisTranslatePipe],
})
export class ExamExerciseOverviewPageComponent extends ExamPageComponent implements OnInit, OnChanges {
protected changeDetectorReference: ChangeDetectorRef = inject(ChangeDetectorRef);
private examParticipationService = inject(ExamParticipationService);

@Input() studentExam: StudentExam;
@Output() onPageChanged = new EventEmitter<{ overViewChange: boolean; exercise: Exercise; forceSave: boolean }>();
studentExam = input.required<StudentExam>();
onPageChanged = output<{
overViewChange: boolean;
exercise: Exercise;
forceSave: boolean;
}>();
getIcon = getIcon;
getIconTooltip = getIconTooltip;
showResultWidth = 10;

examExerciseOverviewItems: ExamExerciseOverviewItem[] = [];

ngOnInit() {
this.studentExam.exercises?.forEach((exercise) => {
this.studentExam().exercises?.forEach((exercise) => {
const item = new ExamExerciseOverviewItem();
item.exercise = exercise;
item.icon = faHourglassHalf;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
@if (exercise) {
@if (exercise()) {
<h3 class="text-align-left fw-normal">
<span>
{{ exercise.exerciseGroup?.title }}
{{ exercise().exerciseGroup?.title }}
</span>
<span
[jhiTranslate]="exercise.bonusPoints ? 'artemisApp.examParticipation.bonus' : 'artemisApp.examParticipation.points'"
[translateValues]="{ points: exercise.maxPoints, bonusPoints: exercise.bonusPoints }"
[jhiTranslate]="exercise().bonusPoints ? 'artemisApp.examParticipation.bonus' : 'artemisApp.examParticipation.points'"
[translateValues]="{ points: exercise().maxPoints, bonusPoints: exercise().bonusPoints }"
>
</span>
@if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) {
<jhi-included-in-score-badge [includedInOverallScore]="exercise.includedInOverallScore" />
@if (exercise().includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) {
<jhi-included-in-score-badge [includedInOverallScore]="exercise().includedInOverallScore" />
}
</h3>
<hr />
<jhi-resizeable-container [examTimeline]="examTimeline">
<jhi-resizeable-container [examTimeline]="examTimeline()">
<!--region Left Panel-->
<span class="exercise-title" left-header>{{ examTimeline ? exercise.title : ('artemisApp.exam.yourSolution' | artemisTranslate) }}</span>
<span class="exercise-title" left-header>{{ examTimeline() ? exercise().title : ('artemisApp.exam.yourSolution' | artemisTranslate) }}</span>
<div left-body class="px-2 pb-2 w-100">
<div class="row">
@if (isActive && !result && exercise && studentSubmission && !readonly) {
@if (isActive && !result && exercise() && studentSubmission() && !readonly()) {
<div class="col-12 col-md-10">
<div class="form-group">
<label for="fileUploadInput" class="form-control-label" jhiTranslate="artemisApp.fileUploadSubmission.selectFile"></label>
Expand All @@ -36,7 +36,7 @@ <h3 class="text-align-left fw-normal">
</div>
</div>
<p class="d-inline-block" jhiTranslate="artemisApp.fileUploadExercise.supportedFileExtensions"></p>
@for (extension of exercise.filePattern!.split(','); track extension) {
@for (extension of exercise().filePattern!.split(','); track extension) {
<div class="d-inline-block">
<span class="ms-1 badge bg-info">
{{ extension | uppercase }}
Expand All @@ -47,18 +47,18 @@ <h3 class="text-align-left fw-normal">
</div>
}
</div>
@if (submittedFileName && studentSubmission?.filePath) {
@if (submittedFileName && studentSubmission()?.filePath) {
<div class="card-text">
<h6 jhiTranslate="artemisApp.fileUploadSubmission.submittedFile" [translateValues]="{ filename: submittedFileName }"></h6>
<a class="text-primary" (click)="downloadFile(studentSubmission!.filePath!)" jhiTranslate="artemisApp.fileUploadSubmission.download"></a>
<a class="text-primary" (click)="downloadFile(studentSubmission()!.filePath!)" jhiTranslate="artemisApp.fileUploadSubmission.download"></a>
@if (submittedFileExtension) {
<span class="ms-2 badge bg-info">
{{ submittedFileExtension | uppercase }}
</span>
}
</div>
}
@if (!submittedFileName && examTimeline) {
@if (!submittedFileName && examTimeline()) {
<div>
<h6 jhiTranslate="artemisApp.timeline.fileUploadNotSubmitted"></h6>
</div>
Expand All @@ -70,8 +70,8 @@ <h6 jhiTranslate="artemisApp.timeline.fileUploadNotSubmitted"></h6>
<span id="problem-statement" right-header jhiTranslate="artemisApp.exercise.problemStatement"></span>
<ng-container right-body>
<!-- problem statement update & difference highlighter -->
@if (exercise) {
<jhi-exam-exercise-update-highlighter [exercise]="exercise" (problemStatementUpdateEvent)="updateProblemStatement($event)" />
@if (exercise()) {
<jhi-exam-exercise-update-highlighter [exercise]="exercise()" (problemStatementUpdateEvent)="updateProblemStatement($event)" />
}
@if (problemStatementHtml) {
<p class="mb-3 markdown-preview">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, ElementRef, Input, OnInit, ViewChild, inject } from '@angular/core';
import { Component, ElementRef, OnInit, inject, input, model, viewChild } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { AlertService } from 'app/core/util/alert.service';
import dayjs from 'dayjs/esm';
Expand Down Expand Up @@ -46,10 +46,10 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i

exerciseType = ExerciseType.FILE_UPLOAD;

@ViewChild('fileInput', { static: false }) fileInput: ElementRef;
fileInput = viewChild<ElementRef>('fileInput');

@Input() studentSubmission: FileUploadSubmission;
@Input() exercise: FileUploadExercise;
studentSubmission = model.required<FileUploadSubmission>();
exercise = input.required<FileUploadExercise>();
problemStatementHtml: string;

submittedFileName: string;
Expand All @@ -71,7 +71,7 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i
*/
ngOnInit() {
// show submission answers in UI
this.problemStatementHtml = htmlForMarkdown(this.exercise?.problemStatement);
this.problemStatementHtml = htmlForMarkdown(this.exercise()?.problemStatement);
this.updateViewFromSubmission();
}

Expand All @@ -93,14 +93,14 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i
if (event.target.files.length) {
const fileList: FileList = event.target.files;
const submissionFile = fileList[0];
const allowedFileExtensions = this.exercise.filePattern!.split(',');
const allowedFileExtensions = this.exercise().filePattern!.split(',');
if (!allowedFileExtensions.some((extension) => submissionFile.name.toLowerCase().endsWith(extension))) {
this.alertService.error('artemisApp.fileUploadSubmission.fileExtensionError');
} else if (submissionFile.size > MAX_SUBMISSION_FILE_SIZE) {
this.alertService.error('artemisApp.fileUploadSubmission.fileTooBigError', { fileName: submissionFile.name });
} else {
this.submissionFile = submissionFile;
this.studentSubmission.isSynced = false;
this.studentSubmission().isSynced = false;
}
}
}
Expand All @@ -113,22 +113,22 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i
* The exercise is still active if it's due date hasn't passed yet.
*/
get isActive(): boolean {
return this.exercise && (!this.exercise.dueDate || dayjs(this.exercise.dueDate).isSameOrAfter(dayjs()));
return this.exercise() && (!this.exercise().dueDate || dayjs(this.exercise().dueDate).isSameOrAfter(dayjs()));
}

getExerciseId(): number | undefined {
return this.exercise.id;
return this.exercise().id;
}
getExercise(): Exercise {
return this.exercise;
return this.exercise();
}

public hasUnsavedChanges(): boolean {
return !this.studentSubmission.isSynced!;
return !this.studentSubmission().isSynced!;
}

getSubmission(): Submission {
return this.studentSubmission;
return this.studentSubmission();
}

updateSubmissionFromView(): void {
Expand All @@ -139,10 +139,10 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i
* Here the new filePath, which was received from the server, is used to display the name and type of the just uploaded file.
*/
updateViewFromSubmission(): void {
if ((this.studentSubmission.isSynced && this.studentSubmission.filePath) || (this.examTimeline && this.studentSubmission.filePath)) {
if ((this.studentSubmission().isSynced && this.studentSubmission().filePath) || (this.examTimeline() && this.studentSubmission().filePath)) {
// clear submitted file so that it is not displayed in the input (this might be confusing)
this.submissionFile = undefined;
const filePath = this.studentSubmission!.filePath!.split('/');
const filePath = this.studentSubmission()!.filePath!.split('/');
this.submittedFileName = filePath.last()!;
const fileName = this.submittedFileName.split('.');
this.submittedFileExtension = fileName.last()!;
Expand All @@ -157,12 +157,12 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i
if (!this.submissionFile) {
return;
}
this.fileUploadSubmissionService.update(this.studentSubmission as FileUploadSubmission, this.exercise.id!, this.submissionFile).subscribe({
this.fileUploadSubmissionService.update(this.studentSubmission() as FileUploadSubmission, this.exercise().id!, this.submissionFile).subscribe({
next: (res) => {
const submissionFromServer = res.body!;
this.studentSubmission.filePath = submissionFromServer.filePath;
this.studentSubmission.isSynced = true;
this.studentSubmission.submitted = true;
this.studentSubmission().filePath = submissionFromServer.filePath;
this.studentSubmission().isSynced = true;
this.studentSubmission().submitted = true;
this.updateViewFromSubmission();
},
error: () => this.onError(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
@if (exercise) {
@if (exercise()) {
<div class="d-flex justify-content-between align-items-center">
<h3 class="text-align-left fw-normal mb-0">
<span>
{{ exercise.exerciseGroup?.title }}
{{ exercise().exerciseGroup?.title }}
</span>
<span
[jhiTranslate]="exercise.bonusPoints ? 'artemisApp.examParticipation.bonus' : 'artemisApp.examParticipation.points'"
[translateValues]="{ points: exercise.maxPoints, bonusPoints: exercise.bonusPoints }"
[jhiTranslate]="exercise().bonusPoints ? 'artemisApp.examParticipation.bonus' : 'artemisApp.examParticipation.points'"
[translateValues]="{ points: exercise().maxPoints, bonusPoints: exercise().bonusPoints }"
>
</span>
@if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) {
<jhi-included-in-score-badge [includedInOverallScore]="exercise.includedInOverallScore" />
@if (exercise().includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) {
<jhi-included-in-score-badge [includedInOverallScore]="exercise().includedInOverallScore" />
}
</h3>
<jhi-exercise-save-button [submission]="studentSubmission" (save)="notifyTriggerSave()" />
<jhi-exercise-save-button [submission]="studentSubmission()" (save)="notifyTriggerSave()" />
</div>
<hr />

<jhi-resizeable-container class="col-12" [examTimeline]="examTimeline">
<jhi-resizeable-container class="col-12" [examTimeline]="examTimeline()">
<!--region Left Panel-->
<span class="exercise-title" left-header>{{ examTimeline ? exercise.title : ('artemisApp.exam.yourSolution' | artemisTranslate) }}</span>
<span class="exercise-title" left-header>{{ examTimeline() ? exercise().title : ('artemisApp.exam.yourSolution' | artemisTranslate) }}</span>
<div left-body class="submission-container d-flex flex-column ps-2 mt-3 w-100">
<jhi-fullscreen>
<div class="row flex-grow-1">
@if (studentSubmission && isActive) {
@if (studentSubmission() && isActive) {
<div class="col-12 editor-large">
<jhi-modeling-editor
[umlModel]="umlModel"
[diagramType]="exercise.diagramType!"
[diagramType]="exercise().diagramType!"
(onModelChanged)="modelChanged($event)"
[readOnly]="readonly"
[readOnly]="readonly()"
[withExplanation]="true"
[explanation]="explanationText"
(explanationChange)="explanationChanged($event)"
Expand All @@ -45,8 +45,8 @@ <h3 class="text-align-left fw-normal mb-0">
<span right-header jhiTranslate="artemisApp.modelingSubmission.problemStatement"></span>
<!-- problem statement update & difference highlighter -->
<ng-container right-body>
@if (exercise) {
<jhi-exam-exercise-update-highlighter [exercise]="exercise" (problemStatementUpdateEvent)="updateProblemStatement($event)" />
@if (exercise()) {
<jhi-exam-exercise-update-highlighter [exercise]="exercise()" (problemStatementUpdateEvent)="updateProblemStatement($event)" />
}
@if (problemStatementHtml) {
<p class="mb-3 markdown-preview">
Expand Down
Loading
Loading