diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts
index df3eec824ca7..410b0a067886 100644
--- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts
+++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts
@@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { SafeHtml } from '@angular/platform-browser';
import { ProgrammingExerciseBuildConfig } from 'app/entities/programming/programming-exercise-build.config';
-import { Subject, Subscription, of } from 'rxjs';
+import { Subject, Subscription } from 'rxjs';
import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming/programming-exercise.model';
import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service';
import { AlertService, AlertType } from 'app/core/util/alert.service';
@@ -57,9 +57,8 @@ import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-setting
import { Detail } from 'app/detail-overview-list/detail.model';
import { Competency } from 'app/entities/competency.model';
import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.service';
-import { switchMap, tap } from 'rxjs/operators';
+import { mergeMap, tap } from 'rxjs/operators';
import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model';
-import { BuildLogStatisticsDTO } from 'app/entities/programming/build-log-statistics-dto';
@Component({
selector: 'jhi-programming-exercise-detail',
@@ -194,7 +193,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {
this.loadingTemplateParticipationResults = false;
this.loadingSolutionParticipationResults = false;
}),
- switchMap(() => this.profileService.getProfileInfo()),
+ mergeMap(() => this.profileService.getProfileInfo()),
tap(async (profileInfo) => {
if (profileInfo) {
if (this.programmingExercise.projectKey && this.programmingExercise.templateParticipation?.buildPlanId) {
@@ -224,22 +223,14 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {
}
}
}),
- switchMap(() => this.programmingExerciseSubmissionPolicyService.getSubmissionPolicyOfProgrammingExercise(exerciseId!)),
+ mergeMap(() => this.programmingExerciseSubmissionPolicyService.getSubmissionPolicyOfProgrammingExercise(exerciseId!)),
tap((submissionPolicy) => {
this.programmingExercise.submissionPolicy = submissionPolicy;
}),
- switchMap(() => this.programmingExerciseService.getDiffReport(this.programmingExercise.id!)),
+ mergeMap(() => this.programmingExerciseService.getDiffReport(this.programmingExercise.id!)),
tap((gitDiffReport) => {
this.processGitDiffReport(gitDiffReport);
}),
- switchMap(() =>
- this.programmingExercise.isAtLeastEditor ? this.programmingExerciseService.getBuildLogStatistics(exerciseId!) : of([] as BuildLogStatisticsDTO),
- ),
- tap((buildLogStatistics) => {
- if (this.programmingExercise.isAtLeastEditor) {
- this.programmingExercise.buildLogStatistics = buildLogStatistics;
- }
- }),
)
.subscribe({
next: () => {
@@ -248,6 +239,8 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {
this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(
programmingExercise.programmingLanguage,
).plagiarismCheckSupported;
+
+ /** we make sure to await the results of the subscriptions (mergeMap) to only call {@link getExerciseDetails} once */
this.exerciseDetailSections = this.getExerciseDetails();
},
error: (error) => {
@@ -258,6 +251,13 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {
this.exerciseStatisticsSubscription = this.statisticsService.getExerciseStatistics(exerciseId!).subscribe((statistics: ExerciseManagementStatisticsDto) => {
this.doughnutStats = statistics;
});
+
+ if (this.programmingExercise.isAtLeastEditor) {
+ this.buildLogsSubscription = this.programmingExerciseService
+ .getBuildLogStatistics(exerciseId!)
+ .subscribe((buildLogStatistics) => (this.programmingExercise.buildLogStatistics = buildLogStatistics));
+ this.exerciseDetailSections = this.getExerciseDetails();
+ }
});
}
@@ -272,6 +272,13 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy {
this.exerciseStatisticsSubscription?.unsubscribe();
}
+ /**
+ * BE CAREFUL WHEN CALLING THIS METHOD!
+ * This method can cause child components to re-render, which can lead to re-initializations resulting
+ * in unnecessary requests putting load on the server.
+ *
+ * When adding a new call to this method, make sure that no duplicated and unnecessary requests are made.
+ */
getExerciseDetails(): DetailOverviewSection[] {
const exercise = this.programmingExercise;
exercise.buildConfig = this.programmingExerciseBuildConfig;
diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts
index 4fc8013ced0e..18b9e883df71 100644
--- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts
+++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts
@@ -33,6 +33,8 @@ import {
} from 'app/exercises/programming/shared/service/programming-language-feature/programming-language-feature.service';
import { MockRouter } from '../../helpers/mocks/mock-router';
import { BuildConfig } from '../../../../../main/webapp/app/entities/programming/build-config.model';
+import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model';
+import { BuildLogStatisticsDTO } from 'app/entities/programming/build-log-statistics-dto';
describe('ProgrammingExerciseDetailComponent', () => {
let comp: ProgrammingExerciseDetailComponent;
@@ -43,6 +45,8 @@ describe('ProgrammingExerciseDetailComponent', () => {
let profileService: ProfileService;
let programmingLanguageFeatureService: ProgrammingLanguageFeatureService;
let statisticsServiceStub: jest.SpyInstance;
+ let gitDiffReportStub: jest.SpyInstance;
+ let buildLogStatisticsStub: jest.SpyInstance;
let findWithTemplateAndSolutionParticipationStub: jest.SpyInstance;
let router: Router;
let modalService: NgbModal;
@@ -75,6 +79,30 @@ describe('ProgrammingExerciseDetailComponent', () => {
resolvedPostsInPercent: 50,
} as ExerciseManagementStatisticsDto;
+ const gitDiffReport = {
+ templateRepositoryCommitHash: 'x1',
+ solutionRepositoryCommitHash: 'x2',
+ entries: [
+ {
+ previousFilePath: '/src/test.java',
+ filePath: '/src/test.java',
+ previousStartLine: 1,
+ startLine: 1,
+ previousLineCount: 2,
+ lineCount: 2,
+ },
+ ],
+ } as ProgrammingExerciseGitDiffReport;
+
+ const buildLogStatistics = {
+ buildCount: 5,
+ agentSetupDuration: 2.5,
+ testDuration: 3,
+ scaDuration: 2,
+ totalJobDuration: 7.5,
+ dependenciesDownloadedCount: 6,
+ } as BuildLogStatisticsDTO;
+
const profileInfo = {
activeProfiles: [],
} as unknown as ProfileInfo;
@@ -111,6 +139,8 @@ describe('ProgrammingExerciseDetailComponent', () => {
findWithTemplateAndSolutionParticipationStub = jest
.spyOn(exerciseService, 'findWithTemplateAndSolutionParticipationAndLatestResults')
.mockReturnValue(of(new HttpResponse({ body: mockProgrammingExercise })));
+ gitDiffReportStub = jest.spyOn(exerciseService, 'getDiffReport').mockReturnValue(of(gitDiffReport));
+ buildLogStatisticsStub = jest.spyOn(exerciseService, 'getBuildLogStatistics').mockReturnValue(of(buildLogStatistics));
jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(profileInfo));
jest.spyOn(programmingLanguageFeatureService, 'getProgrammingLanguageFeature').mockReturnValue({
@@ -131,6 +161,7 @@ describe('ProgrammingExerciseDetailComponent', () => {
comp.onParticipationChange();
tick();
expect(loadDiffSpy).toHaveBeenCalledOnce();
+ expect(gitDiffReportStub).toHaveBeenCalledOnce();
expect(comp.programmingExercise.coveredLinesRatio).toBe(0.5);
}));
@@ -157,6 +188,25 @@ describe('ProgrammingExerciseDetailComponent', () => {
expect(comp.doughnutStats.resolvedPostsInPercent).toBe(50);
expect(comp.doughnutStats.absoluteAveragePoints).toBe(5);
});
+
+ it.each([true, false])(
+ 'should only call service method to get build log statistics onInit if the user is at least an editor for this exercise',
+ async (isEditor: boolean) => {
+ const programmingExercise = new ProgrammingExercise(new Course(), undefined);
+ programmingExercise.id = 123;
+ programmingExercise.isAtLeastEditor = isEditor;
+ jest.spyOn(exerciseService, 'findWithTemplateAndSolutionParticipationAndLatestResults').mockReturnValue(
+ of({ body: programmingExercise } as unknown as HttpResponse),
+ );
+ comp.ngOnInit();
+ await new Promise((r) => setTimeout(r, 100));
+ if (isEditor) {
+ expect(buildLogStatisticsStub).toHaveBeenCalledOnce();
+ } else {
+ expect(buildLogStatisticsStub).not.toHaveBeenCalled();
+ }
+ },
+ );
});
describe('onInit for exam exercise', () => {