Skip to content

Commit

Permalink
Development: Migrate exam scores module to signals, inject and standa…
Browse files Browse the repository at this point in the history
…lone (#9921)
  • Loading branch information
coolchock authored Jan 5, 2025
1 parent 24bab3c commit e12d70e
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 117 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="row d-flex justify-content-center mb-1">
<div #containerRef class="col-md-8 col-11 align-items-center px-0">
<h6 class="text-center" jhiTranslate="artemisApp.examScores.exerciseGroupTitle" [translateValues]="{ groupTitle: this.averageScores.title }"></h6>
<h6 class="text-center" jhiTranslate="artemisApp.examScores.exerciseGroupTitle" [translateValues]="{ groupTitle: this.averageScores().title }"></h6>
<ngx-charts-bar-horizontal
[roundEdges]="false"
[view]="[containerRef.offsetWidth, 100]"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, OnInit, inject, input } from '@angular/core';
import { StatisticsService } from 'app/shared/statistics-graph/statistics.service';
import { TranslateService } from '@ngx-translate/core';
import { GraphColors } from 'app/entities/statistics.model';
Expand All @@ -9,19 +9,29 @@ import { ActivatedRoute } from '@angular/router';
import { ExerciseType } from 'app/entities/exercise.model';
import { ArtemisNavigationUtilService, navigateToExamExercise } from 'app/utils/navigation.utils';
import { Course } from 'app/entities/course.model';
import { Color, ScaleType } from '@swimlane/ngx-charts';
import { BarChartModule, Color, ScaleType } from '@swimlane/ngx-charts';
import { NgxChartsSingleSeriesDataEntry } from 'app/shared/chart/ngx-charts-datatypes';
import { axisTickFormattingWithPercentageSign } from 'app/shared/statistics-graph/statistics-graph.utils';
import { TranslateDirective } from 'app/shared/language/translate.directive';
import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module';

type NameToValueMap = { [name: string]: any };

@Component({
selector: 'jhi-exam-scores-average-scores-graph',
templateUrl: './exam-scores-average-scores-graph.component.html',
standalone: true,
imports: [TranslateDirective, BarChartModule, ArtemisSharedCommonModule],
})
export class ExamScoresAverageScoresGraphComponent implements OnInit {
@Input() averageScores: AggregatedExerciseGroupResult;
@Input() course: Course;
private navigationUtilService = inject(ArtemisNavigationUtilService);
private activatedRoute = inject(ActivatedRoute);
private service = inject(StatisticsService);
private translateService = inject(TranslateService);
private localeConversionService = inject(LocaleConversionService);

averageScores = input.required<AggregatedExerciseGroupResult>();
course = input.required<Course>();

courseId: number;
examId: number;
Expand All @@ -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'];
Expand All @@ -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 });
Expand All @@ -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');
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ <h2 class="font-weigth-bold">
@if (examScoreDTO.maxPoints) {
<h6>{{ 'artemisApp.examScores.maxPoints' | artemisTranslate }}: {{ localize(examScoreDTO.maxPoints) }},</h6>
}
<a [routerLink]="['/course-management', course!.id, 'exams', examScoreDTO.examId, 'exercise-groups']">
<a [routerLink]="['/course-management', course?.id, 'exams', examScoreDTO.examId, 'exercise-groups']">
<h6 class="ms-2">{{ exerciseGroups.length }} {{ 'artemisApp.examScores.noExerciseGroups' | artemisTranslate }}</h6>
</a>
<h6>,</h6>
<a [routerLink]="['/course-management', course!.id, 'exams', examScoreDTO.examId, 'students']">
<a [routerLink]="['/course-management', course?.id, 'exams', examScoreDTO.examId, 'students']">
<h6 class="ms-2">{{ aggregatedExamResults.noOfRegisteredUsers }} {{ 'artemisApp.examScores.registered' | artemisTranslate }}</h6>
</a>
</div>
Expand Down
36 changes: 21 additions & 15 deletions src/main/webapp/app/exam/exam-scores/exam-scores.component.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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[];
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
}

/**
Expand Down
28 changes: 0 additions & 28 deletions src/main/webapp/app/exam/exam-scores/exam-scores.module.ts

This file was deleted.

22 changes: 12 additions & 10 deletions src/main/webapp/app/exam/exam-scores/exam-scores.route.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
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',
},
canActivate: [UserRouteAccessService],
},
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ArtemisExamScoresRoutingModule {}
5 changes: 2 additions & 3 deletions src/main/webapp/app/exam/manage/exam-management.module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -67,17 +66,17 @@ 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
providers: [ArtemisDurationFromSecondsPipe],
imports: [
RouterModule.forChild(ENTITY_STATES),
ArtemisTextExerciseModule,
ArtemisExamScoresModule,
ArtemisSharedModule,
FormDateTimePickerModule,
ArtemisSharedComponentModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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: () => {
Expand All @@ -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', () => {
Expand All @@ -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 = [];
Expand Down Expand Up @@ -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');
Expand Down
Loading

0 comments on commit e12d70e

Please sign in to comment.