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 @@

@if (examScoreDTO.maxPoints) {

{{ 'artemisApp.examScores.maxPoints' | artemisTranslate }}: {{ localize(examScoreDTO.maxPoints) }},
} - +
{{ exerciseGroups.length }} {{ 'artemisApp.examScores.noExerciseGroups' | artemisTranslate }}
,
- +
{{ aggregatedExamResults.noOfRegisteredUsers }} {{ 'artemisApp.examScores.registered' | artemisTranslate }}
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(() => {