From e12d70ea9972b86d1e4986f46d631a558592314b Mon Sep 17 00:00:00 2001
From: Michal Kawka <73854755+coolchock@users.noreply.github.com>
Date: Sun, 5 Jan 2025 20:52:50 +0100
Subject: [PATCH] Development: Migrate exam scores module to signals, inject
and standalone (#9921)
---
...scores-average-scores-graph.component.html | 2 +-
...m-scores-average-scores-graph.component.ts | 38 ++++++++--------
.../exam-scores/exam-scores.component.html | 4 +-
.../exam/exam-scores/exam-scores.component.ts | 36 +++++++++-------
.../exam/exam-scores/exam-scores.module.ts | 28 ------------
.../app/exam/exam-scores/exam-scores.route.ts | 22 +++++-----
.../app/exam/manage/exam-management.module.ts | 5 +--
...res-average-scores-graph.component.spec.ts | 33 +++++++-------
.../exam-scores/exam-scores.component.spec.ts | 43 ++++++++-----------
9 files changed, 94 insertions(+), 117 deletions(-)
delete mode 100644 src/main/webapp/app/exam/exam-scores/exam-scores.module.ts
diff --git a/src/main/webapp/app/exam/exam-scores/exam-scores-average-scores-graph.component.html b/src/main/webapp/app/exam/exam-scores/exam-scores-average-scores-graph.component.html
index 35ab48c1854a..958122437f2b 100644
--- a/src/main/webapp/app/exam/exam-scores/exam-scores-average-scores-graph.component.html
+++ b/src/main/webapp/app/exam/exam-scores/exam-scores-average-scores-graph.component.html
@@ -1,6 +1,6 @@
-
+
();
+ course = input.required();
courseId: number;
examId: number;
@@ -39,14 +49,6 @@ export class ExamScoresAverageScoresGraphComponent implements OnInit {
xScaleMax = 100;
lookup: NameToValueMap = {};
- constructor(
- private navigationUtilService: ArtemisNavigationUtilService,
- private activatedRoute: ActivatedRoute,
- private service: StatisticsService,
- private translateService: TranslateService,
- private localeConversionService: LocaleConversionService,
- ) {}
-
ngOnInit(): void {
this.activatedRoute.params.subscribe((params) => {
this.courseId = +params['courseId'];
@@ -56,12 +58,12 @@ export class ExamScoresAverageScoresGraphComponent implements OnInit {
}
private initializeChart(): void {
- this.lookup[this.averageScores.title] = { absoluteValue: this.averageScores.averagePoints! };
- const exerciseGroupAverage = this.averageScores.averagePercentage ? this.averageScores.averagePercentage : 0;
- this.ngxData.push({ name: this.averageScores.title, value: exerciseGroupAverage });
+ this.lookup[this.averageScores().title] = { absoluteValue: this.averageScores().averagePoints! };
+ const exerciseGroupAverage = this.averageScores().averagePercentage ?? 0;
+ this.ngxData.push({ name: this.averageScores().title, value: exerciseGroupAverage });
this.ngxColor.domain.push(this.determineColor(true, exerciseGroupAverage));
this.xScaleMax = this.xScaleMax > exerciseGroupAverage ? this.xScaleMax : exerciseGroupAverage;
- this.averageScores.exerciseResults.forEach((exercise) => {
+ this.averageScores().exerciseResults.forEach((exercise) => {
const exerciseAverage = exercise.averagePercentage ?? 0;
this.xScaleMax = this.xScaleMax > exerciseAverage ? this.xScaleMax : exerciseAverage;
this.ngxData.push({ name: exercise.exerciseId + ' ' + exercise.title, value: exerciseAverage });
@@ -77,14 +79,14 @@ export class ExamScoresAverageScoresGraphComponent implements OnInit {
}
roundAndPerformLocalConversion(points: number | undefined) {
- return this.localeConversionService.toLocaleString(roundValueSpecifiedByCourseSettings(points, this.course), this.course!.accuracyOfScores!);
+ return this.localeConversionService.toLocaleString(roundValueSpecifiedByCourseSettings(points, this.course()), this.course()!.accuracyOfScores!);
}
/**
* We navigate to the exercise scores page when the user clicks on a data point
*/
navigateToExercise(exerciseId: number, exerciseType: ExerciseType) {
- navigateToExamExercise(this.navigationUtilService, this.courseId, this.examId, this.averageScores.exerciseGroupId, exerciseType, exerciseId, 'scores');
+ navigateToExamExercise(this.navigationUtilService, this.courseId, this.examId, this.averageScores().exerciseGroupId, exerciseType, exerciseId, 'scores');
}
/**
diff --git a/src/main/webapp/app/exam/exam-scores/exam-scores.component.html b/src/main/webapp/app/exam/exam-scores/exam-scores.component.html
index 2fe2e07c3e3a..40b23cc91347 100644
--- a/src/main/webapp/app/exam/exam-scores/exam-scores.component.html
+++ b/src/main/webapp/app/exam/exam-scores/exam-scores.component.html
@@ -35,11 +35,11 @@
diff --git a/src/main/webapp/app/exam/exam-scores/exam-scores.component.ts b/src/main/webapp/app/exam/exam-scores/exam-scores.component.ts
index 94a6f375b3b7..97bb7fb19d39 100644
--- a/src/main/webapp/app/exam/exam-scores/exam-scores.component.ts
+++ b/src/main/webapp/app/exam/exam-scores/exam-scores.component.ts
@@ -1,8 +1,8 @@
-import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core';
import { Subscription, forkJoin, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ExamManagementService } from 'app/exam/manage/exam-management.service';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, RouterLink } from '@angular/router';
import { SortService } from 'app/shared/service/sort.service';
import { download, generateCsv, mkConfig } from 'export-to-csv';
import {
@@ -58,6 +58,11 @@ import {
USERNAME_KEY,
} from 'app/shared/export/export-constants';
import { BonusStrategy } from 'app/entities/bonus.model';
+import { ExamScoresAverageScoresGraphComponent } from 'app/exam/exam-scores/exam-scores-average-scores-graph.component';
+import { ArtemisParticipantScoresModule } from 'app/shared/participant-scores/participant-scores.module';
+import { ExportModule } from 'app/shared/export/export.module';
+import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module';
+import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module';
export enum MedianType {
PASSED,
@@ -70,8 +75,21 @@ export enum MedianType {
templateUrl: './exam-scores.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./exam-scores.component.scss', '../../shared/chart/vertical-bar-chart.scss'],
+ standalone: true,
+ imports: [RouterLink, ArtemisSharedComponentModule, ArtemisSharedCommonModule, ExamScoresAverageScoresGraphComponent, ArtemisParticipantScoresModule, ExportModule],
})
export class ExamScoresComponent implements OnInit, OnDestroy {
+ private route = inject(ActivatedRoute);
+ private examService = inject(ExamManagementService);
+ private sortService = inject(SortService);
+ private alertService = inject(AlertService);
+ private changeDetector = inject(ChangeDetectorRef);
+ private languageHelper = inject(JhiLanguageHelper);
+ private localeConversionService = inject(LocaleConversionService);
+ private participantScoresService = inject(ParticipantScoresService);
+ private gradingSystemService = inject(GradingSystemService);
+ private courseManagementService = inject(CourseManagementService);
+
public examScoreDTO: ExamScoreDTO;
public exerciseGroups: ExerciseGroup[];
public studentResults: StudentResult[];
@@ -130,18 +148,6 @@ export class ExamScoresComponent implements OnInit, OnDestroy {
faExclamationTriangle = faExclamationTriangle;
private languageChangeSubscription?: Subscription;
- constructor(
- private route: ActivatedRoute,
- private examService: ExamManagementService,
- private sortService: SortService,
- private alertService: AlertService,
- private changeDetector: ChangeDetectorRef,
- private languageHelper: JhiLanguageHelper,
- private localeConversionService: LocaleConversionService,
- private participantScoresService: ParticipantScoresService,
- private gradingSystemService: GradingSystemService,
- private courseManagementService: CourseManagementService,
- ) {}
ngOnInit() {
this.route.params.subscribe((params) => {
@@ -676,7 +682,7 @@ export class ExamScoresComponent implements OnInit, OnDestroy {
* Localizes a number, e.g. switching the decimal separator
*/
localize(numberToLocalize: number): string {
- return this.localeConversionService.toLocaleString(numberToLocalize, this.course!.accuracyOfScores!);
+ return this.localeConversionService.toLocaleString(numberToLocalize, this.course?.accuracyOfScores);
}
/**
diff --git a/src/main/webapp/app/exam/exam-scores/exam-scores.module.ts b/src/main/webapp/app/exam/exam-scores/exam-scores.module.ts
deleted file mode 100644
index e439030cb776..000000000000
--- a/src/main/webapp/app/exam/exam-scores/exam-scores.module.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { NgModule } from '@angular/core';
-import { ExamScoresComponent } from './exam-scores.component';
-import { ArtemisSharedModule } from 'app/shared/shared.module';
-import { ArtemisExamScoresRoutingModule } from 'app/exam/exam-scores/exam-scores.route';
-import { ArtemisDataTableModule } from 'app/shared/data-table/data-table.module';
-import { NgxDatatableModule } from '@siemens/ngx-datatable';
-import { ArtemisResultModule } from 'app/exercises/shared/result/result.module';
-import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module';
-import { ExamScoresAverageScoresGraphComponent } from 'app/exam/exam-scores/exam-scores-average-scores-graph.component';
-import { BarChartModule } from '@swimlane/ngx-charts';
-import { ArtemisParticipantScoresModule } from 'app/shared/participant-scores/participant-scores.module';
-import { ExportModule } from 'app/shared/export/export.module';
-
-@NgModule({
- declarations: [ExamScoresComponent, ExamScoresAverageScoresGraphComponent],
- imports: [
- ArtemisSharedModule,
- ArtemisExamScoresRoutingModule,
- ArtemisDataTableModule,
- NgxDatatableModule,
- ArtemisResultModule,
- ArtemisSharedComponentModule,
- BarChartModule,
- ArtemisParticipantScoresModule,
- ExportModule,
- ],
-})
-export class ArtemisExamScoresModule {}
diff --git a/src/main/webapp/app/exam/exam-scores/exam-scores.route.ts b/src/main/webapp/app/exam/exam-scores/exam-scores.route.ts
index 2635eae62e5f..c0b815477e42 100644
--- a/src/main/webapp/app/exam/exam-scores/exam-scores.route.ts
+++ b/src/main/webapp/app/exam/exam-scores/exam-scores.route.ts
@@ -1,13 +1,21 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { UserRouteAccessService } from 'app/core/auth/user-route-access-service';
+import { Route, Routes } from '@angular/router';
import { ExamScoresComponent } from 'app/exam/exam-scores/exam-scores.component';
import { Authority } from 'app/shared/constants/authority.constants';
+import { UserRouteAccessService } from 'app/core/auth/user-route-access-service';
-const routes: Routes = [
+export const examScoresRoute: Route[] = [
{
path: ':examId/scores',
component: ExamScoresComponent,
+ },
+];
+
+const EXAM_SCORES_ROUTES = [...examScoresRoute];
+
+export const examScoresState: Routes = [
+ {
+ path: '',
+ children: EXAM_SCORES_ROUTES,
data: {
authorities: [Authority.ADMIN, Authority.INSTRUCTOR],
pageTitle: 'artemisApp.examScores.title',
@@ -15,9 +23,3 @@ const routes: Routes = [
canActivate: [UserRouteAccessService],
},
];
-
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule],
-})
-export class ArtemisExamScoresRoutingModule {}
diff --git a/src/main/webapp/app/exam/manage/exam-management.module.ts b/src/main/webapp/app/exam/manage/exam-management.module.ts
index 5590adf04e27..e95bb9d2d5ee 100644
--- a/src/main/webapp/app/exam/manage/exam-management.module.ts
+++ b/src/main/webapp/app/exam/manage/exam-management.module.ts
@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
-import { ArtemisExamScoresModule } from 'app/exam/exam-scores/exam-scores.module';
import { ExamManagementComponent } from 'app/exam/manage/exam-management.component';
import { examManagementState } from 'app/exam/manage/exam-management.route';
import { ExamUpdateComponent } from 'app/exam/manage/exams/exam-update.component';
@@ -67,9 +66,10 @@ import { ArtemisProgrammingExerciseModule } from 'app/exercises/programming/shar
import { DetailModule } from 'app/detail-overview-list/detail.module';
import { ArtemisDurationFromSecondsPipe } from 'app/shared/pipes/artemis-duration-from-seconds.pipe';
import { NoDataComponent } from 'app/shared/no-data-component';
+import { examScoresState } from 'app/exam/exam-scores/exam-scores.route';
import { GitDiffLineStatComponent } from 'app/exercises/programming/git-diff-report/git-diff-line-stat.component';
-const ENTITY_STATES = [...examManagementState];
+const ENTITY_STATES = [...examManagementState, ...examScoresState];
@NgModule({
// TODO: For better modularization we could define an exercise module with the corresponding exam routes
@@ -77,7 +77,6 @@ const ENTITY_STATES = [...examManagementState];
imports: [
RouterModule.forChild(ENTITY_STATES),
ArtemisTextExerciseModule,
- ArtemisExamScoresModule,
ArtemisSharedModule,
FormDateTimePickerModule,
ArtemisSharedComponentModule,
diff --git a/src/test/javascript/spec/component/exam/exam-scores/exam-scores-average-scores-graph.component.spec.ts b/src/test/javascript/spec/component/exam/exam-scores/exam-scores-average-scores-graph.component.spec.ts
index f962626e8e37..49da1a162d28 100644
--- a/src/test/javascript/spec/component/exam/exam-scores/exam-scores-average-scores-graph.component.spec.ts
+++ b/src/test/javascript/spec/component/exam/exam-scores/exam-scores-average-scores-graph.component.spec.ts
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateService } from '@ngx-translate/core';
-import { MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks';
+import { MockDirective, MockModule, MockProvider } from 'ng-mocks';
import { of } from 'rxjs';
import { HttpResponse } from '@angular/common/http';
import { ExamScoresAverageScoresGraphComponent } from 'app/exam/exam-scores/exam-scores-average-scores-graph.component';
@@ -9,7 +9,6 @@ import { MockTranslateService } from '../../../helpers/mocks/service/mock-transl
import { AggregatedExerciseGroupResult, AggregatedExerciseResult } from 'app/exam/exam-scores/exam-score-dtos.model';
import { CourseManagementService } from 'app/course/manage/course-management.service';
import { BarChartModule } from '@swimlane/ngx-charts';
-import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
import { GraphColors } from 'app/entities/statistics.model';
import { NgxChartsSingleSeriesDataEntry } from 'app/shared/chart/ngx-charts-datatypes';
import { ExerciseType } from 'app/entities/exercise.model';
@@ -56,7 +55,7 @@ describe('ExamScoresAverageScoresGraphComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ArtemisTestModule, MockModule(BarChartModule), RouterModule.forRoot([])],
- declarations: [ExamScoresAverageScoresGraphComponent, MockPipe(ArtemisTranslatePipe), MockDirective(TranslateDirective)],
+ declarations: [ExamScoresAverageScoresGraphComponent, MockDirective(TranslateDirective)],
providers: [
MockProvider(CourseManagementService, {
find: () => {
@@ -70,16 +69,14 @@ describe('ExamScoresAverageScoresGraphComponent', () => {
}),
{ provide: TranslateService, useClass: MockTranslateService },
],
- })
- .compileComponents()
- .then(() => {
- fixture = TestBed.createComponent(ExamScoresAverageScoresGraphComponent);
- component = fixture.componentInstance;
- navigateToExerciseMock = jest.spyOn(component, 'navigateToExercise').mockImplementation();
-
- component.averageScores = returnValue;
- fixture.detectChanges();
- });
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ExamScoresAverageScoresGraphComponent);
+ component = fixture.componentInstance;
+ navigateToExerciseMock = jest.spyOn(component, 'navigateToExercise').mockImplementation();
+
+ fixture.componentRef.setInput('averageScores', returnValue);
+ fixture.detectChanges();
});
it('should set ngx data objects and bar colors correctly', () => {
@@ -99,8 +96,9 @@ describe('ExamScoresAverageScoresGraphComponent', () => {
});
const adaptExpectedData = (averagePoints: number, newColor: string, expectedColorDomain: string[], expectedData: NgxChartsSingleSeriesDataEntry[]) => {
- component.averageScores.averagePoints = averagePoints;
- component.averageScores.averagePercentage = averagePoints * 10;
+ component.averageScores().averagePoints = averagePoints;
+ component.averageScores().averagePercentage = averagePoints * 10;
+
expectedColorDomain[0] = newColor;
expectedData[0].value = averagePoints * 10;
component.ngxColor.domain = [];
@@ -146,7 +144,10 @@ describe('ExamScoresAverageScoresGraphComponent', () => {
it('should look up absolute value', () => {
const roundAndPerformLocalConversionSpy = jest.spyOn(component, 'roundAndPerformLocalConversion');
- component.course = { accuracyOfScores: 2 };
+ const updatedCourse = {
+ accuracyOfScores: 2,
+ };
+ fixture.componentRef.setInput('course', updatedCourse);
component.lookup['test'] = { absoluteValue: 40 };
const result = component.lookupAbsoluteValue('test');
diff --git a/src/test/javascript/spec/component/exam/exam-scores/exam-scores.component.spec.ts b/src/test/javascript/spec/component/exam/exam-scores/exam-scores.component.spec.ts
index df9ddaa86480..3d82fa3ba673 100644
--- a/src/test/javascript/spec/component/exam/exam-scores/exam-scores.component.spec.ts
+++ b/src/test/javascript/spec/component/exam/exam-scores/exam-scores.component.spec.ts
@@ -1,5 +1,6 @@
-import { HttpResponse } from '@angular/common/http';
+import { HttpResponse, provideHttpClient } from '@angular/common/http';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { provideHttpClientTesting } from '@angular/common/http/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { TranslateService } from '@ngx-translate/core';
@@ -14,15 +15,14 @@ import {
ExerciseResult,
StudentResult,
} from 'app/exam/exam-scores/exam-score-dtos.model';
+import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ExamScoresComponent, MedianType } from 'app/exam/exam-scores/exam-scores.component';
import { ExamManagementService } from 'app/exam/manage/exam-management.service';
-import { HelpIconComponent } from 'app/shared/components/help-icon.component';
-import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.directive';
import { ParticipantScoresService, ScoresDTO } from 'app/shared/participant-scores/participant-scores.service';
import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
import { SortService } from 'app/shared/service/sort.service';
import { cloneDeep } from 'lodash-es';
-import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks';
import { EMPTY, of } from 'rxjs';
import { GradingSystemService } from 'app/grading-system/grading-system.service';
import { GradingScale } from 'app/entities/grading-scale.model';
@@ -35,7 +35,6 @@ import { CourseManagementService } from 'app/course/manage/course-management.ser
import { MockRouter } from '../../../helpers/mocks/mock-router';
import { AccountService } from 'app/core/auth/account.service';
import { MockRouterLinkDirective } from '../../../helpers/mocks/directive/mock-router-link.directive';
-import { ParticipantScoresDistributionComponent } from 'app/shared/participant-scores/participant-scores-distribution/participant-scores-distribution.component';
import { LocaleConversionService } from 'app/shared/service/locale-conversion.service';
import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils';
import { CsvDecimalSeparator, CsvExportOptions, CsvFieldSeparator, CsvQuoteStrings } from 'app/shared/export/export-modal.component';
@@ -55,9 +54,9 @@ import {
REGISTRATION_NUMBER_KEY,
USERNAME_KEY,
} from 'app/shared/export/export-constants';
-import { ExportButtonComponent } from 'app/shared/export/export-button.component';
import { PlagiarismVerdict } from 'app/exercises/shared/plagiarism/types/PlagiarismVerdict';
import { BonusStrategy } from 'app/entities/bonus.model';
+import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service';
describe('ExamScoresComponent', () => {
let fixture: ComponentFixture
;
@@ -276,30 +275,28 @@ describe('ExamScoresComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
+ imports: [MockModule(BrowserAnimationsModule)],
declarations: [
ExamScoresComponent,
MockPipe(ArtemisTranslatePipe),
MockComponent(FaIconComponent),
- MockComponent(HelpIconComponent),
- MockComponent(ExportButtonComponent),
MockDirective(TranslateDirective),
MockDirective(SortByDirective),
MockDirective(SortDirective),
- MockDirective(DeleteButtonDirective),
MockComponent(ExamScoresAverageScoresGraphComponent),
MockRouterLinkDirective,
- MockComponent(ParticipantScoresDistributionComponent),
],
providers: [
{ provide: ActivatedRoute, useValue: { params: of({ courseId: 1, examId: 1 }) } },
{ provide: Router, useClass: MockRouter },
+ { provide: TranslateService, useClass: MockTranslateService },
+ provideHttpClient(),
+ provideHttpClientTesting(),
MockProvider(AccountService),
MockProvider(ArtemisNavigationUtilService),
- MockProvider(TranslateService),
MockProvider(ExamManagementService),
MockProvider(SortService),
MockProvider(AlertService),
- MockProvider(ParticipantScoresService),
MockProvider(GradingSystemService, {
findGradingScaleForExam: () => {
return of(
@@ -328,18 +325,16 @@ describe('ExamScoresComponent', () => {
},
}),
],
- })
- .compileComponents()
- .then(() => {
- fixture = TestBed.createComponent(ExamScoresComponent);
- comp = fixture.componentInstance;
- examService = fixture.debugElement.injector.get(ExamManagementService);
- gradingSystemService = fixture.debugElement.injector.get(GradingSystemService);
- const participationScoreService = fixture.debugElement.injector.get(ParticipantScoresService);
- findExamScoresSpy = jest
- .spyOn(participationScoreService, 'findExamScores')
- .mockReturnValue(of(new HttpResponse({ body: [examScoreStudent1, examScoreStudent2, examScoreStudent3] })));
- });
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ExamScoresComponent);
+ comp = fixture.componentInstance;
+ examService = fixture.debugElement.injector.get(ExamManagementService);
+ gradingSystemService = fixture.debugElement.injector.get(GradingSystemService);
+ const participationScoreService = fixture.debugElement.injector.get(ParticipantScoresService);
+ findExamScoresSpy = jest
+ .spyOn(participationScoreService, 'findExamScores')
+ .mockReturnValue(of(new HttpResponse({ body: [examScoreStudent1, examScoreStudent2, examScoreStudent3] })));
});
afterEach(() => {