From c6b76291d83c404826624ffbd5773f1cb2d4d383 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 13 Aug 2024 20:51:12 +0200 Subject: [PATCH 01/52] added template for advanced errorfiltering --- ...-exercise-configure-grading.component.html | 14 ++- ...ng-exercise-configure-grading.component.ts | 2 +- .../programming-exercise-grading.module.ts | 2 + .../testcase-analysis.component.html | 27 ++++++ .../testcase-analysis.component.scss | 13 +++ .../testcase-analysis.component.ts | 91 +++++++++++++++++++ .../webapp/i18n/de/programmingExercise.json | 9 ++ .../webapp/i18n/en/programmingExercise.json | 9 ++ .../testcase-analysis.component.spec.ts | 22 +++++ 9 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts create mode 100644 src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index 6ee637d20532..2f77d71de09d 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -16,12 +16,15 @@

Submission Policy +
+ Test Analysis +
- @if (activeTab !== 'submission-policy') { + @if (activeTab !== 'submission-policy' && activeTab !== 'test-analysis') { } - @if (programmingExercise.isAtLeastInstructor) { + @if (programmingExercise.isAtLeastInstructor && activeTab !== 'test-analysis') {
} +
+ @if (activeTab === 'test-analysis') { +
+ +
+ } +
} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts index c242b861d14e..bd4411d72234 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts @@ -56,7 +56,7 @@ const DefaultFieldValues: { [key: string]: number } = { [EditableField.MAX_PENALTY]: 0, }; -export type GradingTab = 'test-cases' | 'code-analysis' | 'submission-policy'; +export type GradingTab = 'test-cases' | 'code-analysis' | 'submission-policy' | 'test-analysis'; export type Table = 'testCases' | 'codeAnalysis'; diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts index 3f91d95f8fc2..5ac548cca160 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts @@ -19,6 +19,7 @@ import { SubmissionPolicyUpdateModule } from 'app/exercises/shared/submission-po import { ProgrammingExerciseGradingTasksTableComponent } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-grading-tasks-table.component'; import { BarChartModule } from '@swimlane/ngx-charts'; import { ProgrammingExerciseTaskComponent } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task/programming-exercise-task.component'; +import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component'; @NgModule({ imports: [ @@ -42,6 +43,7 @@ import { ProgrammingExerciseTaskComponent } from 'app/exercises/programming/mana ProgrammingExerciseGradingTableActionsComponent, ProgrammingExerciseGradingSubmissionPolicyConfigurationActionsComponent, ProgrammingExerciseGradingTasksTableComponent, + TestcaseAnalysisComponent, TestCasePassedBuildsChartComponent, CategoryIssuesChartComponent, TestCaseDistributionChartComponent, diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html new file mode 100644 index 000000000000..401d48f9640e --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html @@ -0,0 +1,27 @@ +
+

{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.title' | artemisTranslate: { exerciseTitle: exerciseTitle } }}

+

Feature currently in development. Following data is testdata

+ + + + + + + + + + + + @for (item of exampleArray; track item) { + + + + + + + + } + +
{{ item.occurrence }}{{ item.feedback }}{{ item.task }}{{ item.testcase }}{{ item.errorCategory }}
+
{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.totalItems' | artemisTranslate: { count: exampleArray.length } }}
+
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss new file mode 100644 index 000000000000..03cb39d409a1 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss @@ -0,0 +1,13 @@ +.container { + margin: 20px; +} + +h2 { + margin-bottom: 20px; +} + +.table { + width: 100%; + margin-bottom: 1rem; + color: #212529; +} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts new file mode 100644 index 000000000000..7f105a632c14 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -0,0 +1,91 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'jhi-testcase-analysis', + templateUrl: './testcase-analysis.component.html', + styleUrls: ['./testcase-analysis.component.scss'], +}) +export class TestcaseAnalysisComponent { + @Input() exerciseTitle?: string; + + exampleArray = [ + { + occurrence: '39 (39%)', + feedback: "The expected method 'insert' of the class 'RecursiveNode' with the parameters: [\"int\"] was not found...", + task: 1, + testcase: 'testMethods[RecursiveNode]', + errorCategory: 'Student Error', + }, + { + occurrence: '20 (20%)', + feedback: "The expected method 'getLeftNode' of the class 'RecursiveNode' with no parameters was not found ...", + task: 1, + testcase: 'testMethods[RecursiveNode]', + errorCategory: 'Student Error', + }, + { + occurrence: '20 (20%)', + feedback: 'Could not find the constructor with the parameters: [ int ] in the class RecursiveTree because the ...', + task: 5, + testcase: 'testRecursiveTreeAdd()', + errorCategory: 'Student Error', + }, + { + occurrence: '20 (20%)', + feedback: 'Could not instantiate the class RecursiveTree because access to its constructor with the parameters: ...', + task: 5, + testcase: 'testRecursiveTreeAdd()', + errorCategory: 'Student Error', + }, + { + occurrence: '10 (10%)', + feedback: 'Could not find the constructor with the parameters: [ int ] in the class RecursiveTree because the ...', + task: 3, + testcase: 'testRecursiveTreeIsEmpty()', + errorCategory: 'Student Error', + }, + { + occurrence: '10 (10%)', + feedback: 'Could not instantiate the class RecursiveTree because access to its constructor with the parameters: ...', + task: 3, + testcase: 'testRecursiveTreeIsEmpty()', + errorCategory: 'Student Error', + }, + { + occurrence: '10 (10%)', + feedback: 'Could not find the constructor with the parameters: [ int ] in the class RecursiveNode because the ...', + task: 6, + testcase: 'testContainsMethodRecursiveNode()', + errorCategory: 'Student Error', + }, + { + occurrence: '10 (10%)', + feedback: 'Could not instantiate the class RecursiveNode because access to its constructor with the parameters: ...', + task: 6, + testcase: 'testContainsMethodRecursiveNode()', + errorCategory: 'Student Error', + }, + { + occurrence: '10 (10%)', + feedback: 'Could not find the constructor with the parameters: [ int ] in the class RecursiveTree because the ...', + task: 7, + testcase: 'testRecursiveTreeSize()', + errorCategory: 'Student Error', + }, + { + occurrence: '10 (10%)', + feedback: 'Could not instantiate the class RecursiveNode because access to its constructor with the parameters: ...', + task: 7, + testcase: 'testRecursiveTreeSize()', + errorCategory: 'Student Error', + }, + { + occurrence: '1 (1%)', + feedback: 'Failed: "Unwanted Statement Found. For Each Statement was found."', + task: 7, + testcase: 'testRecursiveTreeSize()', + errorCategory: 'AST Error', + }, + { occurrence: '1 (1%)', feedback: 'Security Exception: “.....”', task: 7, testcase: 'testRecursiveTreeSize()', errorCategory: 'ARES Error' }, + ]; +} diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index ae25116ddc7a..b84d1ca8a19a 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -306,6 +306,15 @@ "testType": "Type", "passedPercent": "Bestanden %" }, + "testAnalysis": { + "title": "Error Analyse für {{exerciseTitle}}", + "occurrence": "Häufigkeit", + "testCaseFeedback": "Test Fall Feedback", + "task": "Task", + "testcase": "Test Fall", + "errorCategory": "Error Kategorie", + "totalItems": "Insgesamt {{count}} items" + }, "help": { "name": "Aufgabennamen werden fett geschrieben, während Testnamen normal sind. Ob es ein Aufgabenname oder Testname ist hängt davon ab, ob die Reihe eine Aufgabe oder einen Test darstellt.", "state": "Gibt an, ob Issues in dieser Kategorie den Studierenden angezeigt und bewertet werden sollen.", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index e199355c4a82..03089a8f6f96 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -308,6 +308,15 @@ "testType": "Type", "passedPercent": "Passed %" }, + "testAnalysis": { + "title": "Error Analysis for {{exerciseTitle}}", + "occurrence": "Occurrence", + "testCaseFeedback": "Test Case Feedback", + "task": "Task", + "testcase": "Testcase", + "errorCategory": "Error Category", + "totalItems": "Total {{count}} items" + }, "help": { "name": "Task names are written in bold whereas Test names are normal. Task or test name depending on whether the row is a task or test.", "state": "Determines whether issues in this category should be shown to the students and used for grading.", diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts new file mode 100644 index 000000000000..954b89741d40 --- /dev/null +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component'; + +describe('TestcaseAnalysisComponent', () => { + let component: TestcaseAnalysisComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestcaseAnalysisComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TestcaseAnalysisComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); From e4b0294474aa0c28919c2ab1dca4e08f6419a2c0 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Wed, 14 Aug 2024 20:04:10 +0200 Subject: [PATCH 02/52] removed example table and added real exercise data --- .../testcase-analysis.component.html | 15 +- .../testcase-analysis.component.ts | 139 +++++++----------- 2 files changed, 64 insertions(+), 90 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html index 401d48f9640e..1fb119d6e807 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html @@ -1,6 +1,5 @@

{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.title' | artemisTranslate: { exerciseTitle: exerciseTitle } }}

-

Feature currently in development. Following data is testdata

@@ -12,16 +11,16 @@

Feature currently in development. Following data is testdata

- @for (item of exampleArray; track item) { + @for (item of feedbacks; track item) { - - - - - + + + + + }
{{ item.occurrence }}{{ item.feedback }}{{ item.task }}{{ item.testcase }}{{ item.errorCategory }}{{ item.count }}{{ item.detailText }}11Student Error
-
{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.totalItems' | artemisTranslate: { count: exampleArray.length } }}
+
{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.totalItems' | artemisTranslate: { count: feedbacks.length } }}
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 7f105a632c14..743aa9bfb679 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -1,91 +1,66 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; +import { Feedback } from 'app/entities/feedback.model'; +import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service'; +import { ResultService } from 'app/exercises/shared/result/result.service'; +import { Participation } from 'app/entities/participation/participation.model'; +import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; + +type FeedbackDetail = { + count: number; + detailText: string; +}; @Component({ selector: 'jhi-testcase-analysis', templateUrl: './testcase-analysis.component.html', styleUrls: ['./testcase-analysis.component.scss'], }) -export class TestcaseAnalysisComponent { +export class TestcaseAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; - exampleArray = [ - { - occurrence: '39 (39%)', - feedback: "The expected method 'insert' of the class 'RecursiveNode' with the parameters: [\"int\"] was not found...", - task: 1, - testcase: 'testMethods[RecursiveNode]', - errorCategory: 'Student Error', - }, - { - occurrence: '20 (20%)', - feedback: "The expected method 'getLeftNode' of the class 'RecursiveNode' with no parameters was not found ...", - task: 1, - testcase: 'testMethods[RecursiveNode]', - errorCategory: 'Student Error', - }, - { - occurrence: '20 (20%)', - feedback: 'Could not find the constructor with the parameters: [ int ] in the class RecursiveTree because the ...', - task: 5, - testcase: 'testRecursiveTreeAdd()', - errorCategory: 'Student Error', - }, - { - occurrence: '20 (20%)', - feedback: 'Could not instantiate the class RecursiveTree because access to its constructor with the parameters: ...', - task: 5, - testcase: 'testRecursiveTreeAdd()', - errorCategory: 'Student Error', - }, - { - occurrence: '10 (10%)', - feedback: 'Could not find the constructor with the parameters: [ int ] in the class RecursiveTree because the ...', - task: 3, - testcase: 'testRecursiveTreeIsEmpty()', - errorCategory: 'Student Error', - }, - { - occurrence: '10 (10%)', - feedback: 'Could not instantiate the class RecursiveTree because access to its constructor with the parameters: ...', - task: 3, - testcase: 'testRecursiveTreeIsEmpty()', - errorCategory: 'Student Error', - }, - { - occurrence: '10 (10%)', - feedback: 'Could not find the constructor with the parameters: [ int ] in the class RecursiveNode because the ...', - task: 6, - testcase: 'testContainsMethodRecursiveNode()', - errorCategory: 'Student Error', - }, - { - occurrence: '10 (10%)', - feedback: 'Could not instantiate the class RecursiveNode because access to its constructor with the parameters: ...', - task: 6, - testcase: 'testContainsMethodRecursiveNode()', - errorCategory: 'Student Error', - }, - { - occurrence: '10 (10%)', - feedback: 'Could not find the constructor with the parameters: [ int ] in the class RecursiveTree because the ...', - task: 7, - testcase: 'testRecursiveTreeSize()', - errorCategory: 'Student Error', - }, - { - occurrence: '10 (10%)', - feedback: 'Could not instantiate the class RecursiveNode because access to its constructor with the parameters: ...', - task: 7, - testcase: 'testRecursiveTreeSize()', - errorCategory: 'Student Error', - }, - { - occurrence: '1 (1%)', - feedback: 'Failed: "Unwanted Statement Found. For Each Statement was found."', - task: 7, - testcase: 'testRecursiveTreeSize()', - errorCategory: 'AST Error', - }, - { occurrence: '1 (1%)', feedback: 'Security Exception: “.....”', task: 7, testcase: 'testRecursiveTreeSize()', errorCategory: 'ARES Error' }, - ]; + feedbacks: FeedbackDetail[] = []; + + constructor( + private participationService: ParticipationService, + private resultService: ResultService, + private programmingExerciseTaskService: ProgrammingExerciseTaskService, + ) {} + + ngOnInit(): void { + if (this.programmingExerciseTaskService.exercise.id != undefined) { + this.participationService.findAllParticipationsByExercise(this.programmingExerciseTaskService.exercise.id, true).subscribe((participationsResponse) => { + this.loadFeedbacks(participationsResponse.body ?? []); + }); + } + } + + loadFeedbacks(participations: Participation[]): void { + participations.forEach((participation) => { + participation.results?.forEach((result) => { + this.resultService.getFeedbackDetailsForResult(participation.id!, result).subscribe((response) => { + const feedbackArray = response.body ?? []; + this.saveFeedbacks(feedbackArray); + }); + }); + }); + } + + saveFeedbacks(feedbackArray: Feedback[]): Feedback[] { + feedbackArray.forEach((feedback) => { + const feedbackText = feedback.text ?? ''; + const existingFeedback = this.feedbacks.find((f) => f.detailText === feedbackText); + if (existingFeedback) { + existingFeedback.count += 1; + existingFeedback.detailText += `\n${feedback.detailText}`; + } else { + this.feedbacks.push({ count: 1, detailText: feedback.detailText ?? '' }); + } + }); + this.sortFeedbacksByCount(); + return feedbackArray; + } + + sortFeedbacksByCount(): void { + this.feedbacks.sort((a, b) => b.count - a.count); + } } From cbf61d9e4e0e8483d31de7604d3fb286372db37e Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Thu, 15 Aug 2024 14:28:03 +0200 Subject: [PATCH 03/52] table works for exercises, tests missing --- .../testcase-analysis.component.html | 7 +-- .../testcase-analysis.component.ts | 53 ++++++++++++++----- .../webapp/i18n/en/programmingExercise.json | 2 +- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html index 1fb119d6e807..f0488ef695ce 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html @@ -13,11 +13,12 @@

{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.title' | ar @for (item of feedbacks; track item) { - {{ item.count }} + {{ item.count }} ({{ getRelativeCount(item.count) | number: '1.0-0' }}%) {{ item.detailText }} - 1 - 1 + {{ item.task }} + {{ item.testcase }} Student Error + } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 743aa9bfb679..23fabb741037 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -4,10 +4,15 @@ import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage import { ResultService } from 'app/exercises/shared/result/result.service'; import { Participation } from 'app/entities/participation/participation.model'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; +import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task'; +import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; +// Define the structure for FeedbackDetail type FeedbackDetail = { count: number; detailText: string; + testcase: string; + task: number; }; @Component({ @@ -17,7 +22,8 @@ type FeedbackDetail = { }) export class TestcaseAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; - + participation: Participation[] = []; + tasks: ProgrammingExerciseTask[] = []; feedbacks: FeedbackDetail[] = []; constructor( @@ -28,39 +34,60 @@ export class TestcaseAnalysisComponent implements OnInit { ngOnInit(): void { if (this.programmingExerciseTaskService.exercise.id != undefined) { + // Find all participations for the programming exercise and instantiate the feedbacks array with the FeedbackDetail structure this.participationService.findAllParticipationsByExercise(this.programmingExerciseTaskService.exercise.id, true).subscribe((participationsResponse) => { - this.loadFeedbacks(participationsResponse.body ?? []); + this.participation = participationsResponse.body ?? []; + this.loadFeedbacks(this.participation); }); } + // Load tasks for the programming exercise + this.tasks = this.programmingExerciseTaskService.updateTasks(); } loadFeedbacks(participations: Participation[]): void { + // Iterate over all participations, get feedback details for each result, and filter them for negative feedback participations.forEach((participation) => { participation.results?.forEach((result) => { this.resultService.getFeedbackDetailsForResult(participation.id!, result).subscribe((response) => { const feedbackArray = response.body ?? []; - this.saveFeedbacks(feedbackArray); + const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); // Filter out positive feedback + this.saveFeedbacks(negativeFeedbackArray); // Save only negative feedback }); }); }); } - saveFeedbacks(feedbackArray: Feedback[]): Feedback[] { + saveFeedbacks(feedbackArray: Feedback[]): void { + // Iterate over all feedback and save them in the feedbacks array + // If a feedback with the corresponding testcase already exists in the list, then the count is incremented; otherwise, a new FeedbackDetail is added feedbackArray.forEach((feedback) => { - const feedbackText = feedback.text ?? ''; - const existingFeedback = this.feedbacks.find((f) => f.detailText === feedbackText); + const feedbackText = feedback.detailText ?? ''; + const existingFeedback = this.feedbacks.find((f) => f.detailText === feedbackText && f.testcase === feedback.testCase?.testName); if (existingFeedback) { - existingFeedback.count += 1; - existingFeedback.detailText += `\n${feedback.detailText}`; + existingFeedback.count += 1; // Increment count if feedback already exists } else { - this.feedbacks.push({ count: 1, detailText: feedback.detailText ?? '' }); + const task = this.findTaskIndexForTestCase(feedback.testCase); // Find the task index for the test case + this.feedbacks.push({ + count: 1, + detailText: feedback.detailText ?? '', + testcase: feedback.testCase?.testName, + task: task, + }); } }); - this.sortFeedbacksByCount(); - return feedbackArray; + this.feedbacks.sort((a, b) => b.count - a.count); // Sort feedback by count in descending order + } + + findTaskIndexForTestCase(testCase?: ProgrammingExerciseTestCase): number | undefined { + if (!testCase) { + return undefined; + } + // Find the index of the task and add 1 to it (to make it a 1-based index) + return this.tasks.findIndex((task) => task.testCases.some((tc) => tc.testName === testCase.testName)) + 1; } - sortFeedbacksByCount(): void { - this.feedbacks.sort((a, b) => b.count - a.count); + // Used to calculate the relative occurrence of a feedback + getRelativeCount(count: number): number { + return (this.participation.length > 0 ? count / this.participation.length : 0) * 100; } } diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 03089a8f6f96..bbbec518ad9b 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -310,7 +310,7 @@ }, "testAnalysis": { "title": "Error Analysis for {{exerciseTitle}}", - "occurrence": "Occurrence", + "occurrence": "Occurrency", "testCaseFeedback": "Test Case Feedback", "task": "Task", "testcase": "Testcase", From 189e3438e810ed00714ef213b2af7ae175059d60 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Thu, 15 Aug 2024 15:30:07 +0200 Subject: [PATCH 04/52] added client side tests --- .../testcase-analysis.component.ts | 2 +- .../testcase-analysis.component.spec.ts | 104 +++++++++++++++++- 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 23fabb741037..d46429f7bd71 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -82,7 +82,7 @@ export class TestcaseAnalysisComponent implements OnInit { if (!testCase) { return undefined; } - // Find the index of the task and add 1 to it (to make it a 1-based index) + // Find the index of the task and add 1 to it (to make it a 1-based index), if 0 is returned then no element was found return this.tasks.findIndex((task) => task.testCases.some((tc) => tc.testName === testCase.testName)) + 1; } diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 954b89741d40..8e23888fa7aa 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -1,22 +1,114 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { of } from 'rxjs'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; +import { ArtemisTestModule } from '../../../test.module'; import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component'; +import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; +import { ResultService } from 'app/exercises/shared/result/result.service'; +import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service'; +import { Participation } from 'app/entities/participation/participation.model'; +import { Feedback } from 'app/entities/feedback.model'; +import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task'; +import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { ButtonComponent } from 'app/shared/components/button.component'; +import { MockComponent, MockPipe } from 'ng-mocks'; describe('TestcaseAnalysisComponent', () => { let component: TestcaseAnalysisComponent; let fixture: ComponentFixture; + let participationService: ParticipationService; + let resultService: ResultService; + + const participationMock: Participation[] = [ + { + id: 1, + results: [{ id: 1 }], + }, + ] as Participation[]; + + const feedbackMock: Feedback[] = [ + { + text: 'Test feedback 1', + positive: false, + detailText: 'Test feedback 1 detail', + testCase: { testName: 'test1' } as ProgrammingExerciseTestCase, + }, + { + text: 'Test feedback 2', + positive: false, + detailText: 'Test feedback 2 detail', + testCase: { testName: 'test2' } as ProgrammingExerciseTestCase, + }, + ] as Feedback[]; + + const tasksMock: ProgrammingExerciseTask[] = [ + { id: 1, taskName: 'Task 1', testCases: [{ testName: 'test1' } as ProgrammingExerciseTestCase] }, + { id: 2, taskName: 'Task 2', testCases: [{ testName: 'test2' } as ProgrammingExerciseTestCase] }, + ] as ProgrammingExerciseTask[]; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TestcaseAnalysisComponent], + beforeEach(() => { + const mockProgrammingExerciseTaskService = { + exercise: { id: 1 }, // Mock the exercise with an id + updateTasks: jest.fn().mockReturnValue(tasksMock), + }; + + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, TranslateModule.forRoot()], + declarations: [TestcaseAnalysisComponent, MockPipe(ArtemisTranslatePipe), MockComponent(ButtonComponent)], + providers: [ + { provide: TranslateService, useClass: MockTranslateService }, + ParticipationService, + ResultService, + { provide: ProgrammingExerciseTaskService, useValue: mockProgrammingExerciseTaskService }, + ], }).compileComponents(); fixture = TestBed.createComponent(TestcaseAnalysisComponent); component = fixture.componentInstance; + participationService = TestBed.inject(ParticipationService); + resultService = TestBed.inject(ResultService); + + jest.spyOn(participationService, 'findAllParticipationsByExercise').mockReturnValue(of({ body: participationMock })); + jest.spyOn(resultService, 'getFeedbackDetailsForResult').mockReturnValue(of({ body: feedbackMock })); + }); + + it('should initialize and load feedbacks correctly', () => { + component.ngOnInit(); fixture.detectChanges(); + + expect(participationService.findAllParticipationsByExercise).toHaveBeenCalled(); + expect(resultService.getFeedbackDetailsForResult).toHaveBeenCalled(); + expect(component.participation).toEqual(participationMock); + expect(component.feedbacks).toHaveLength(2); + expect(component.feedbacks[0].detailText).toBe('Test feedback 1 detail'); + }); + + it('should save feedbacks and sort them by count', () => { + component.saveFeedbacks(feedbackMock); + + expect(component.feedbacks).toHaveLength(2); + expect(component.feedbacks[0].count).toBe(1); + expect(component.feedbacks[1].count).toBe(1); + expect(component.feedbacks[0].detailText).toBe('Test feedback 1 detail'); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should find task index for a given test case', () => { + component.tasks = tasksMock; + const index = component.findTaskIndexForTestCase({ testName: 'test1' } as ProgrammingExerciseTestCase); + expect(index).toBe(1); + + const zeroIndex = component.findTaskIndexForTestCase({ testName: 'test3' } as ProgrammingExerciseTestCase); + expect(zeroIndex).toBe(0); + }); + + it('should calculate relative count correctly', () => { + component.participation = participationMock; + const relativeCount = component.getRelativeCount(1); + expect(relativeCount).toBe(100); + + const zeroRelativeCount = component.getRelativeCount(0); + expect(zeroRelativeCount).toBe(0); }); }); From 9ea9a24bcd88bfd3f242dcf6bc389295ce99c16b Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Thu, 15 Aug 2024 15:40:27 +0200 Subject: [PATCH 05/52] fixed client side tests --- .../testcase-analysis/testcase-analysis.component.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 8e23888fa7aa..c7e87ce623d4 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -14,6 +14,7 @@ import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-t import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ButtonComponent } from 'app/shared/components/button.component'; import { MockComponent, MockPipe } from 'ng-mocks'; +import { HttpResponse } from '@angular/common/http'; describe('TestcaseAnalysisComponent', () => { let component: TestcaseAnalysisComponent; @@ -48,6 +49,9 @@ describe('TestcaseAnalysisComponent', () => { { id: 2, taskName: 'Task 2', testCases: [{ testName: 'test2' } as ProgrammingExerciseTestCase] }, ] as ProgrammingExerciseTask[]; + const participationResponseMock = new HttpResponse({ body: participationMock }); + const feedbackResponseMock = new HttpResponse({ body: feedbackMock }); + beforeEach(() => { const mockProgrammingExerciseTaskService = { exercise: { id: 1 }, // Mock the exercise with an id @@ -70,8 +74,8 @@ describe('TestcaseAnalysisComponent', () => { participationService = TestBed.inject(ParticipationService); resultService = TestBed.inject(ResultService); - jest.spyOn(participationService, 'findAllParticipationsByExercise').mockReturnValue(of({ body: participationMock })); - jest.spyOn(resultService, 'getFeedbackDetailsForResult').mockReturnValue(of({ body: feedbackMock })); + jest.spyOn(participationService, 'findAllParticipationsByExercise').mockReturnValue(of(participationResponseMock)); + jest.spyOn(resultService, 'getFeedbackDetailsForResult').mockReturnValue(of(feedbackResponseMock)); }); it('should initialize and load feedbacks correctly', () => { From ddcc330bbfb43c8fd56973c61d1bfb995c283431 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Thu, 15 Aug 2024 17:38:31 +0200 Subject: [PATCH 06/52] implemented code rabbit feedback --- ...ng-exercise-configure-grading.component.ts | 1 - .../testcase-analysis.component.ts | 57 +++++++++++++------ .../webapp/i18n/en/programmingExercise.json | 2 +- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts index bd4411d72234..a65fc0091b86 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts @@ -57,7 +57,6 @@ const DefaultFieldValues: { [key: string]: number } = { }; export type GradingTab = 'test-cases' | 'code-analysis' | 'submission-policy' | 'test-analysis'; - export type Table = 'testCases' | 'codeAnalysis'; @Component({ diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index d46429f7bd71..d2186842aeb4 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -6,6 +6,8 @@ import { Participation } from 'app/entities/participation/participation.model'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task'; import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; +import { from, of } from 'rxjs'; +import { catchError, mergeMap, toArray } from 'rxjs/operators'; // Define the structure for FeedbackDetail type FeedbackDetail = { @@ -33,9 +35,10 @@ export class TestcaseAnalysisComponent implements OnInit { ) {} ngOnInit(): void { - if (this.programmingExerciseTaskService.exercise.id != undefined) { + const exerciseId = this.programmingExerciseTaskService.exercise?.id; + if (exerciseId !== undefined) { // Find all participations for the programming exercise and instantiate the feedbacks array with the FeedbackDetail structure - this.participationService.findAllParticipationsByExercise(this.programmingExerciseTaskService.exercise.id, true).subscribe((participationsResponse) => { + this.participationService.findAllParticipationsByExercise(exerciseId, true).subscribe((participationsResponse) => { this.participation = participationsResponse.body ?? []; this.loadFeedbacks(this.participation); }); @@ -44,30 +47,48 @@ export class TestcaseAnalysisComponent implements OnInit { this.tasks = this.programmingExerciseTaskService.updateTasks(); } + // Iterate over all participations, get feedback details for each result, and filter them for negative feedback loadFeedbacks(participations: Participation[]): void { - // Iterate over all participations, get feedback details for each result, and filter them for negative feedback - participations.forEach((participation) => { - participation.results?.forEach((result) => { - this.resultService.getFeedbackDetailsForResult(participation.id!, result).subscribe((response) => { - const feedbackArray = response.body ?? []; - const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); // Filter out positive feedback - this.saveFeedbacks(negativeFeedbackArray); // Save only negative feedback - }); + const MAX_CONCURRENT_REQUESTS = 5; // Maximum number of parallel requests + + from(participations) + .pipe( + mergeMap((participation) => { + return from(participation.results ?? []).pipe( + mergeMap((result) => { + return this.resultService.getFeedbackDetailsForResult(participation.id!, result).pipe( + catchError(() => { + return of({ body: [] }); + }), + ); + }, MAX_CONCURRENT_REQUESTS), + ); + }, MAX_CONCURRENT_REQUESTS), + toArray(), + ) + .subscribe((responses) => { + const feedbackArray = responses.flatMap((response) => response.body ?? []); + const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); // Filter out positive feedback + this.saveFeedbacks(negativeFeedbackArray); // Save only negative feedback }); - }); } + // Iterate over all feedback and save them in the feedbacks array + // If a feedback with the corresponding testcase already exists in the list, then the count is incremented; otherwise, a new FeedbackDetail is added saveFeedbacks(feedbackArray: Feedback[]): void { - // Iterate over all feedback and save them in the feedbacks array - // If a feedback with the corresponding testcase already exists in the list, then the count is incremented; otherwise, a new FeedbackDetail is added + const feedbackMap: Map = new Map(); + feedbackArray.forEach((feedback) => { const feedbackText = feedback.detailText ?? ''; - const existingFeedback = this.feedbacks.find((f) => f.detailText === feedbackText && f.testcase === feedback.testCase?.testName); - if (existingFeedback) { + const testcase = feedback.testCase?.testName ?? ''; + const key = `${feedbackText}_${testcase}`; + + if (feedbackMap.has(key)) { + const existingFeedback = feedbackMap.get(key)!; existingFeedback.count += 1; // Increment count if feedback already exists } else { const task = this.findTaskIndexForTestCase(feedback.testCase); // Find the task index for the test case - this.feedbacks.push({ + feedbackMap.set(key, { count: 1, detailText: feedback.detailText ?? '', testcase: feedback.testCase?.testName, @@ -75,7 +96,9 @@ export class TestcaseAnalysisComponent implements OnInit { }); } }); - this.feedbacks.sort((a, b) => b.count - a.count); // Sort feedback by count in descending order + + // Convert map values to array and sort feedback by count in descending order + this.feedbacks = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); } findTaskIndexForTestCase(testCase?: ProgrammingExerciseTestCase): number | undefined { diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index bbbec518ad9b..03089a8f6f96 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -310,7 +310,7 @@ }, "testAnalysis": { "title": "Error Analysis for {{exerciseTitle}}", - "occurrence": "Occurrency", + "occurrence": "Occurrence", "testCaseFeedback": "Test Case Feedback", "task": "Task", "testcase": "Testcase", From 46dea01433a6b29cfe34ba8397b97ebcf8d0f812 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Thu, 15 Aug 2024 17:49:08 +0200 Subject: [PATCH 07/52] implemented code rabbit feedback --- .../testcase-analysis/testcase-analysis.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index d2186842aeb4..49953d040012 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -84,8 +84,10 @@ export class TestcaseAnalysisComponent implements OnInit { const key = `${feedbackText}_${testcase}`; if (feedbackMap.has(key)) { - const existingFeedback = feedbackMap.get(key)!; - existingFeedback.count += 1; // Increment count if feedback already exists + const existingFeedback = feedbackMap.get(key); + if (existingFeedback) { + existingFeedback.count += 1; + } // Increment count if feedback already exists } else { const task = this.findTaskIndexForTestCase(feedback.testCase); // Find the task index for the test case feedbackMap.set(key, { From cd01d6ace35dd1f8004706537e81c7ea982b1fee Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Thu, 15 Aug 2024 18:14:49 +0200 Subject: [PATCH 08/52] code coverage --- .../grading/testcase-analysis/testcase-analysis.component.ts | 2 +- .../testcase-analysis/testcase-analysis.component.spec.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 49953d040012..f6f61bf421fe 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -105,7 +105,7 @@ export class TestcaseAnalysisComponent implements OnInit { findTaskIndexForTestCase(testCase?: ProgrammingExerciseTestCase): number | undefined { if (!testCase) { - return undefined; + return 0; } // Find the index of the task and add 1 to it (to make it a 1-based index), if 0 is returned then no element was found return this.tasks.findIndex((task) => task.testCases.some((tc) => tc.testName === testCase.testName)) + 1; diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index c7e87ce623d4..e9e53d013cf3 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -105,6 +105,9 @@ describe('TestcaseAnalysisComponent', () => { const zeroIndex = component.findTaskIndexForTestCase({ testName: 'test3' } as ProgrammingExerciseTestCase); expect(zeroIndex).toBe(0); + + const undefinedIndex = component.findTaskIndexForTestCase({ testName: undefined } as ProgrammingExerciseTestCase); + expect(undefinedIndex).toBe(0); }); it('should calculate relative count correctly', () => { From 5b5363550221b223180d2c60da8c908dd9c6686a Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Thu, 15 Aug 2024 18:38:02 +0200 Subject: [PATCH 09/52] code coverage --- .../testcase-analysis/testcase-analysis.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index e9e53d013cf3..bf8ea5683653 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -106,7 +106,7 @@ describe('TestcaseAnalysisComponent', () => { const zeroIndex = component.findTaskIndexForTestCase({ testName: 'test3' } as ProgrammingExerciseTestCase); expect(zeroIndex).toBe(0); - const undefinedIndex = component.findTaskIndexForTestCase({ testName: undefined } as ProgrammingExerciseTestCase); + const undefinedIndex = component.findTaskIndexForTestCase(undefined); expect(undefinedIndex).toBe(0); }); From 5e2246e7642c1450f82256c256e5c63b124745c1 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Thu, 15 Aug 2024 19:05:12 +0200 Subject: [PATCH 10/52] code coverage --- .../testcase-analysis.component.spec.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index bf8ea5683653..be6abc07be52 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../test.module'; @@ -42,6 +42,12 @@ describe('TestcaseAnalysisComponent', () => { detailText: 'Test feedback 2 detail', testCase: { testName: 'test2' } as ProgrammingExerciseTestCase, }, + { + text: 'Test feedback 1', + positive: false, + detailText: 'Test feedback 1 detail', + testCase: { testName: 'test1' } as ProgrammingExerciseTestCase, + }, ] as Feedback[]; const tasksMock: ProgrammingExerciseTask[] = [ @@ -87,13 +93,14 @@ describe('TestcaseAnalysisComponent', () => { expect(component.participation).toEqual(participationMock); expect(component.feedbacks).toHaveLength(2); expect(component.feedbacks[0].detailText).toBe('Test feedback 1 detail'); + expect(component.feedbacks[1].detailText).toBe('Test feedback 2 detail'); }); it('should save feedbacks and sort them by count', () => { component.saveFeedbacks(feedbackMock); expect(component.feedbacks).toHaveLength(2); - expect(component.feedbacks[0].count).toBe(1); + expect(component.feedbacks[0].count).toBe(2); expect(component.feedbacks[1].count).toBe(1); expect(component.feedbacks[0].detailText).toBe('Test feedback 1 detail'); }); @@ -118,4 +125,13 @@ describe('TestcaseAnalysisComponent', () => { const zeroRelativeCount = component.getRelativeCount(0); expect(zeroRelativeCount).toBe(0); }); + + it('should handle errors when loading feedbacks', () => { + jest.spyOn(resultService, 'getFeedbackDetailsForResult').mockReturnValue(throwError('Error')); + + component.loadFeedbacks(participationMock); + + expect(resultService.getFeedbackDetailsForResult).toHaveBeenCalled(); + expect(component.feedbacks).toHaveLength(0); + }); }); From 68ba2cbc6e093c1eff1d1e83bba0c47cc9f7792b Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 16 Aug 2024 13:40:27 +0200 Subject: [PATCH 11/52] implemented feedback --- ...ming-exercise-configure-grading.component.html | 14 ++++++-------- .../testcase-analysis.component.html | 8 ++++---- .../testcase-analysis.component.scss | 9 --------- .../testcase-analysis.component.ts | 7 +------ src/main/webapp/i18n/de/programmingExercise.json | 2 ++ src/main/webapp/i18n/en/programmingExercise.json | 2 ++ .../testcase-analysis.component.spec.ts | 15 +++------------ 7 files changed, 18 insertions(+), 39 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index 2f77d71de09d..ace525c60088 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -6,25 +6,25 @@

- Test Cases +
@if (programmingExercise.staticCodeAnalysisEnabled) {
- Code Analysis +
}
- Submission Policy +
- Test Analysis +
- @if (activeTab !== 'submission-policy' && activeTab !== 'test-analysis') { + @if (activeTab === 'test-cases' || activeTab === 'code-analysis') {
@if (activeTab === 'test-analysis') { -
- -
+ }
} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html index f0488ef695ce..f625267c3bcb 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html @@ -1,5 +1,5 @@ -
-

{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.title' | artemisTranslate: { exerciseTitle: exerciseTitle } }}

+
+

@@ -13,7 +13,7 @@

{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.title' | ar

@for (item of feedbacks; track item) { - + @@ -23,5 +23,5 @@

{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.title' | ar }

{{ item.count }} ({{ getRelativeCount(item.count) | number: '1.0-0' }}%){{ item.count }} ({{ (this.participation.length > 0 ? item.count / this.participation.length : 0) * 100 | number: '1.0-0' }}%) {{ item.detailText }} {{ item.task }} {{ item.testcase }}
-
{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.totalItems' | artemisTranslate: { count: feedbacks.length } }}
+
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss index 03cb39d409a1..a2737fdf3f38 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss @@ -1,13 +1,4 @@ -.container { - margin: 20px; -} - -h2 { - margin-bottom: 20px; -} - .table { width: 100%; margin-bottom: 1rem; - color: #212529; } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index f6f61bf421fe..652ccced1a19 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -103,16 +103,11 @@ export class TestcaseAnalysisComponent implements OnInit { this.feedbacks = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); } - findTaskIndexForTestCase(testCase?: ProgrammingExerciseTestCase): number | undefined { + findTaskIndexForTestCase(testCase?: ProgrammingExerciseTestCase): number { if (!testCase) { return 0; } // Find the index of the task and add 1 to it (to make it a 1-based index), if 0 is returned then no element was found return this.tasks.findIndex((task) => task.testCases.some((tc) => tc.testName === testCase.testName)) + 1; } - - // Used to calculate the relative occurrence of a feedback - getRelativeCount(count: number): number { - return (this.participation.length > 0 ? count / this.participation.length : 0) * 100; - } } diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index b84d1ca8a19a..a6e825d75e13 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -260,6 +260,7 @@ "settingNegative": "Der Testfall {{testCase}} darf keine Einstellungen mit negativen Werten haben." }, "categories": { + "titleHeader": "Code-Analyse", "title": "Code-Analyse-Kategorien", "notGraded": "Nicht bewertet.", "noFeedback": "Ohne sichtbares Feedback.", @@ -307,6 +308,7 @@ "passedPercent": "Bestanden %" }, "testAnalysis": { + "titleHeader": "Test Analyse", "title": "Error Analyse für {{exerciseTitle}}", "occurrence": "Häufigkeit", "testCaseFeedback": "Test Fall Feedback", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 03089a8f6f96..7ed52b0d69d0 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -262,6 +262,7 @@ "settingNegative": "Test case {{testCase}} must not have settings set to negative values." }, "categories": { + "titleHeader": "Code Analysis", "title": "Code Analysis Categories", "notGraded": "Not graded.", "noFeedback": "No visible feedback.", @@ -309,6 +310,7 @@ "passedPercent": "Passed %" }, "testAnalysis": { + "titleHeader": "Test Analysis", "title": "Error Analysis for {{exerciseTitle}}", "occurrence": "Occurrence", "testCaseFeedback": "Test Case Feedback", diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index be6abc07be52..f76d19ecae0b 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -11,10 +11,10 @@ import { Participation } from 'app/entities/participation/participation.model'; import { Feedback } from 'app/entities/feedback.model'; import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task'; import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ButtonComponent } from 'app/shared/components/button.component'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { HttpResponse } from '@angular/common/http'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; describe('TestcaseAnalysisComponent', () => { let component: TestcaseAnalysisComponent; @@ -66,7 +66,7 @@ describe('TestcaseAnalysisComponent', () => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, TranslateModule.forRoot()], - declarations: [TestcaseAnalysisComponent, MockPipe(ArtemisTranslatePipe), MockComponent(ButtonComponent)], + declarations: [TestcaseAnalysisComponent, MockComponent(ButtonComponent), TranslateDirective], providers: [ { provide: TranslateService, useClass: MockTranslateService }, ParticipationService, @@ -117,15 +117,6 @@ describe('TestcaseAnalysisComponent', () => { expect(undefinedIndex).toBe(0); }); - it('should calculate relative count correctly', () => { - component.participation = participationMock; - const relativeCount = component.getRelativeCount(1); - expect(relativeCount).toBe(100); - - const zeroRelativeCount = component.getRelativeCount(0); - expect(zeroRelativeCount).toBe(0); - }); - it('should handle errors when loading feedbacks', () => { jest.spyOn(resultService, 'getFeedbackDetailsForResult').mockReturnValue(throwError('Error')); From 2a7a9e2e8486ebf240d02801e9cb16d287648fef Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 16 Aug 2024 22:39:12 +0200 Subject: [PATCH 12/52] implemented feedback --- .../www1/artemis/web/rest/ResultResource.java | 57 +++++++++++++++++ .../programming-exercise-grading.module.ts | 2 +- .../testcase-analysis.component.html | 4 +- .../testcase-analysis.component.ts | 64 ++++++------------- .../exercises/shared/result/result.service.ts | 9 +++ .../ResultServiceIntegrationTest.java | 58 +++++++++++++++++ .../testcase-analysis.component.spec.ts | 46 ++++++------- 7 files changed, 165 insertions(+), 75 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 37b9afb94432..fb3b9ed41c94 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -5,6 +5,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -276,4 +278,59 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo return ResponseEntity.created(new URI("/api/results/" + savedResult.getId())) .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, savedResult.getId().toString())).body(savedResult); } + + /** + * GET /exercises/:exerciseId/feedback-details : get all feedback details and participations for an exercise. + * + * @param exerciseId The ID of the exercise + * @return A response entity containing feedback details and participations + */ + @GetMapping("exercises/{exerciseId}/feedback-details") + @EnforceAtLeastTutor + public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { + log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); + Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); + + // Get all Student participations for the exercise + Set participation; + if (exercise.isTeamMode()) { + participation = studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsWithTeamInformation(exercise.getId()); + } + else { + participation = studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsAndAssessmentNote(exercise.getId()); + } + removeSubmissionAndExerciseData(participation); + + List allFeedback = new ArrayList<>(); + + // For each participation, get the feedback from its latest result + for (StudentParticipation singleParticipation : participation) { + Result latestResult = singleParticipation.getResults().stream().max(Comparator.comparing(Result::getCompletionDate)).orElse(null); + + if (latestResult != null) { + List feedback = getResultDetails(singleParticipation.getId(), latestResult.getId()).getBody(); + if (feedback != null) { + allFeedback.addAll(feedback); + } + } + } + + FeedbackDetailsResponse response = new FeedbackDetailsResponse(allFeedback, participation); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + private void removeSubmissionAndExerciseData(Set participations) { + // remove unnecessary data to reduce response size + participations.forEach(participation -> { + participation.setSubmissionCount(participation.getSubmissions().size()); + participation.setSubmissions(null); + }); + participations.stream().filter(participation -> participation.getParticipant() != null).peek(participation -> { + participation.setExercise(null); + }); + } + + public record FeedbackDetailsResponse(List feedback, Set participation) { + } + } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts index 5ac548cca160..596127ddd9dc 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts @@ -34,6 +34,7 @@ import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grad ArtemisProgrammingExerciseActionsModule, SubmissionPolicyUpdateModule, BarChartModule, + TestcaseAnalysisComponent, ], declarations: [ ProgrammingExerciseConfigureGradingComponent, @@ -43,7 +44,6 @@ import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grad ProgrammingExerciseGradingTableActionsComponent, ProgrammingExerciseGradingSubmissionPolicyConfigurationActionsComponent, ProgrammingExerciseGradingTasksTableComponent, - TestcaseAnalysisComponent, TestCasePassedBuildsChartComponent, CategoryIssuesChartComponent, TestCaseDistributionChartComponent, diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html index f625267c3bcb..5dc1f7748e3b 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html @@ -11,7 +11,7 @@

- @for (item of feedbacks; track item) { + @for (item of feedback; track item) { {{ item.count }} ({{ (this.participation.length > 0 ? item.count / this.participation.length : 0) * 100 | number: '1.0-0' }}%) {{ item.detailText }} @@ -23,5 +23,5 @@

+
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 652ccced1a19..538e3b74a154 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -1,15 +1,12 @@ import { Component, Input, OnInit } from '@angular/core'; import { Feedback } from 'app/entities/feedback.model'; -import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service'; import { ResultService } from 'app/exercises/shared/result/result.service'; -import { Participation } from 'app/entities/participation/participation.model'; -import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; +import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service'; import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task'; import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; -import { from, of } from 'rxjs'; -import { catchError, mergeMap, toArray } from 'rxjs/operators'; +import { Participation } from 'app/entities/participation/participation.model'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; -// Define the structure for FeedbackDetail type FeedbackDetail = { count: number; detailText: string; @@ -21,15 +18,16 @@ type FeedbackDetail = { selector: 'jhi-testcase-analysis', templateUrl: './testcase-analysis.component.html', styleUrls: ['./testcase-analysis.component.scss'], + standalone: true, + imports: [ArtemisSharedModule], }) export class TestcaseAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; participation: Participation[] = []; tasks: ProgrammingExerciseTask[] = []; - feedbacks: FeedbackDetail[] = []; + feedback: FeedbackDetail[] = []; constructor( - private participationService: ParticipationService, private resultService: ResultService, private programmingExerciseTaskService: ProgrammingExerciseTaskService, ) {} @@ -37,45 +35,22 @@ export class TestcaseAnalysisComponent implements OnInit { ngOnInit(): void { const exerciseId = this.programmingExerciseTaskService.exercise?.id; if (exerciseId !== undefined) { - // Find all participations for the programming exercise and instantiate the feedbacks array with the FeedbackDetail structure - this.participationService.findAllParticipationsByExercise(exerciseId, true).subscribe((participationsResponse) => { - this.participation = participationsResponse.body ?? []; - this.loadFeedbacks(this.participation); - }); + this.loadFeedbackDetails(exerciseId); } - // Load tasks for the programming exercise this.tasks = this.programmingExerciseTaskService.updateTasks(); } - // Iterate over all participations, get feedback details for each result, and filter them for negative feedback - loadFeedbacks(participations: Participation[]): void { - const MAX_CONCURRENT_REQUESTS = 5; // Maximum number of parallel requests + loadFeedbackDetails(exerciseId: number): void { + this.resultService.getFeedbackDetailsForExercise(exerciseId).subscribe((response) => { + const feedbackArray = response.body?.feedback ?? []; + this.participation = response.body?.participation ?? []; - from(participations) - .pipe( - mergeMap((participation) => { - return from(participation.results ?? []).pipe( - mergeMap((result) => { - return this.resultService.getFeedbackDetailsForResult(participation.id!, result).pipe( - catchError(() => { - return of({ body: [] }); - }), - ); - }, MAX_CONCURRENT_REQUESTS), - ); - }, MAX_CONCURRENT_REQUESTS), - toArray(), - ) - .subscribe((responses) => { - const feedbackArray = responses.flatMap((response) => response.body ?? []); - const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); // Filter out positive feedback - this.saveFeedbacks(negativeFeedbackArray); // Save only negative feedback - }); + const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); + this.saveFeedback(negativeFeedbackArray); + }); } - // Iterate over all feedback and save them in the feedbacks array - // If a feedback with the corresponding testcase already exists in the list, then the count is incremented; otherwise, a new FeedbackDetail is added - saveFeedbacks(feedbackArray: Feedback[]): void { + saveFeedback(feedbackArray: Feedback[]): void { const feedbackMap: Map = new Map(); feedbackArray.forEach((feedback) => { @@ -87,9 +62,9 @@ export class TestcaseAnalysisComponent implements OnInit { const existingFeedback = feedbackMap.get(key); if (existingFeedback) { existingFeedback.count += 1; - } // Increment count if feedback already exists + } } else { - const task = this.findTaskIndexForTestCase(feedback.testCase); // Find the task index for the test case + const task = this.findTaskIndexForTestCase(feedback.testCase); feedbackMap.set(key, { count: 1, detailText: feedback.detailText ?? '', @@ -98,16 +73,13 @@ export class TestcaseAnalysisComponent implements OnInit { }); } }); - - // Convert map values to array and sort feedback by count in descending order - this.feedbacks = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); + this.feedback = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); } findTaskIndexForTestCase(testCase?: ProgrammingExerciseTestCase): number { if (!testCase) { return 0; } - // Find the index of the task and add 1 to it (to make it a 1-based index), if 0 is returned then no element was found return this.tasks.findIndex((task) => task.testCases.some((tc) => tc.testName === testCase.testName)) + 1; } } diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index 72390f6979c8..a01cdf9dc7d9 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -46,6 +46,11 @@ export interface IResultService { triggerDownloadCSV: (rows: string[], csvFileName: string) => void; } +interface FeedbackDetailsResponse { + feedback: Feedback[]; + participation: Participation[]; +} + @Injectable({ providedIn: 'root' }) export class ResultService implements IResultService { private exerciseResourceUrl = 'api/exercises'; @@ -218,6 +223,10 @@ export class ResultService implements IResultService { ); } + getFeedbackDetailsForExercise(exerciseId: number): Observable> { + return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); + } + public convertResultDatesFromClient(result: Result): Result { return Object.assign({}, result, { completionDate: convertDateFromClient(result.completionDate), diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index 56e3d0339bda..aa76a67f8403 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -60,6 +60,7 @@ import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; +import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.FeedbackRepository; import de.tum.in.www1.artemis.repository.FileUploadExerciseRepository; import de.tum.in.www1.artemis.repository.GradingCriterionRepository; @@ -71,6 +72,8 @@ import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.repository.TextExerciseRepository; +import de.tum.in.www1.artemis.service.ResultService; +import de.tum.in.www1.artemis.web.rest.ResultResource; import de.tum.in.www1.artemis.web.rest.dto.ResultWithPointsPerGradingCriterionDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -126,6 +129,12 @@ class ResultServiceIntegrationTest extends AbstractSpringIntegrationLocalCILocal @Autowired private ExamUtilService examUtilService; + @Autowired + private ExerciseRepository exerciseRepository; + + @Autowired + private ResultService resultService; + private Course course; private ProgrammingExercise programmingExercise; @@ -722,4 +731,53 @@ void testGetAssessmentCountByCorrectionRoundForProgrammingExercise() { assertThat(assessments[0].inTime()).isEqualTo(1); // correction round 1 assertThat(assessments[1].inTime()).isEqualTo(1); // correction round 2 } + + @Test + @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") + void testGetAllFeedbackDetailsForExercise() throws Exception { + ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); + StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, "student1"); + Result result = participationUtilService.addResultToParticipation(null, null, participation); + participationUtilService.addSampleFeedbackToResults(result); + + ResultResource.FeedbackDetailsResponse response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, + ResultResource.FeedbackDetailsResponse.class); + + assertThat(response.feedback()).isNotEmpty(); + assertThat(response.participation()).hasSize(1); + } + + @Test + @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") + void testGetAllFeedbackDetailsForExercise_NoFeedback() throws Exception { + ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); + StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, "student1"); + + ResultResource.FeedbackDetailsResponse response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, + ResultResource.FeedbackDetailsResponse.class); + + assertThat(response.feedback()).isEmpty(); + assertThat(response.participation()).hasSize(1); + } + + @Test + @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") + void testGetAllFeedbackDetailsForExercise_NoParticipations() throws Exception { + ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); + + ResultResource.FeedbackDetailsResponse response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, + ResultResource.FeedbackDetailsResponse.class); + + assertThat(response.feedback()).isEmpty(); + assertThat(response.participation()).isEmpty(); + } + + @Test + @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") + void testGetAllFeedbackDetailsForExercise_InvalidExerciseId() throws Exception { + long invalidExerciseId = 9999L; + + request.get("/api/exercises/" + invalidExerciseId + "/feedback-details", HttpStatus.NOT_FOUND, ResultResource.FeedbackDetailsResponse.class); + } + } diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index f76d19ecae0b..6db934ab0bea 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -4,7 +4,6 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../test.module'; import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component'; -import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; import { ResultService } from 'app/exercises/shared/result/result.service'; import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service'; import { Participation } from 'app/entities/participation/participation.model'; @@ -14,12 +13,10 @@ import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-t import { ButtonComponent } from 'app/shared/components/button.component'; import { MockComponent } from 'ng-mocks'; import { HttpResponse } from '@angular/common/http'; -import { TranslateDirective } from 'app/shared/language/translate.directive'; describe('TestcaseAnalysisComponent', () => { let component: TestcaseAnalysisComponent; let fixture: ComponentFixture; - let participationService: ParticipationService; let resultService: ResultService; const participationMock: Participation[] = [ @@ -55,8 +52,9 @@ describe('TestcaseAnalysisComponent', () => { { id: 2, taskName: 'Task 2', testCases: [{ testName: 'test2' } as ProgrammingExerciseTestCase] }, ] as ProgrammingExerciseTask[]; - const participationResponseMock = new HttpResponse({ body: participationMock }); - const feedbackResponseMock = new HttpResponse({ body: feedbackMock }); + const feedbackDetailsResponseMock = new HttpResponse({ + body: { feedback: feedbackMock, participation: participationMock }, + }); beforeEach(() => { const mockProgrammingExerciseTaskService = { @@ -66,10 +64,9 @@ describe('TestcaseAnalysisComponent', () => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, TranslateModule.forRoot()], - declarations: [TestcaseAnalysisComponent, MockComponent(ButtonComponent), TranslateDirective], + declarations: [TestcaseAnalysisComponent, MockComponent(ButtonComponent)], providers: [ { provide: TranslateService, useClass: MockTranslateService }, - ParticipationService, ResultService, { provide: ProgrammingExerciseTaskService, useValue: mockProgrammingExerciseTaskService }, ], @@ -77,32 +74,29 @@ describe('TestcaseAnalysisComponent', () => { fixture = TestBed.createComponent(TestcaseAnalysisComponent); component = fixture.componentInstance; - participationService = TestBed.inject(ParticipationService); resultService = TestBed.inject(ResultService); - jest.spyOn(participationService, 'findAllParticipationsByExercise').mockReturnValue(of(participationResponseMock)); - jest.spyOn(resultService, 'getFeedbackDetailsForResult').mockReturnValue(of(feedbackResponseMock)); + jest.spyOn(resultService, 'getFeedbackDetailsForExercise').mockReturnValue(of(feedbackDetailsResponseMock)); }); - it('should initialize and load feedbacks correctly', () => { + it('should initialize and load feedback details correctly', () => { component.ngOnInit(); fixture.detectChanges(); - expect(participationService.findAllParticipationsByExercise).toHaveBeenCalled(); - expect(resultService.getFeedbackDetailsForResult).toHaveBeenCalled(); + expect(resultService.getFeedbackDetailsForExercise).toHaveBeenCalled(); expect(component.participation).toEqual(participationMock); - expect(component.feedbacks).toHaveLength(2); - expect(component.feedbacks[0].detailText).toBe('Test feedback 1 detail'); - expect(component.feedbacks[1].detailText).toBe('Test feedback 2 detail'); + expect(component.feedback).toHaveLength(2); + expect(component.feedback[0].detailText).toBe('Test feedback 1 detail'); + expect(component.feedback[1].detailText).toBe('Test feedback 2 detail'); }); it('should save feedbacks and sort them by count', () => { - component.saveFeedbacks(feedbackMock); + component.saveFeedback(feedbackMock); - expect(component.feedbacks).toHaveLength(2); - expect(component.feedbacks[0].count).toBe(2); - expect(component.feedbacks[1].count).toBe(1); - expect(component.feedbacks[0].detailText).toBe('Test feedback 1 detail'); + expect(component.feedback).toHaveLength(2); + expect(component.feedback[0].count).toBe(2); + expect(component.feedback[1].count).toBe(1); + expect(component.feedback[0].detailText).toBe('Test feedback 1 detail'); }); it('should find task index for a given test case', () => { @@ -117,12 +111,12 @@ describe('TestcaseAnalysisComponent', () => { expect(undefinedIndex).toBe(0); }); - it('should handle errors when loading feedbacks', () => { - jest.spyOn(resultService, 'getFeedbackDetailsForResult').mockReturnValue(throwError('Error')); + it('should handle errors when loading feedback details', () => { + jest.spyOn(resultService, 'getFeedbackDetailsForExercise').mockReturnValue(throwError('Error')); - component.loadFeedbacks(participationMock); + component.loadFeedbackDetails(1); - expect(resultService.getFeedbackDetailsForResult).toHaveBeenCalled(); - expect(component.feedbacks).toHaveLength(0); + expect(resultService.getFeedbackDetailsForExercise).toHaveBeenCalled(); + expect(component.feedback).toHaveLength(0); }); }); From 62be8c9b4e80a3b815873bda073d8688c89f5b6f Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 16 Aug 2024 22:51:31 +0200 Subject: [PATCH 13/52] implemented feedback --- .../programming-exercise-configure-grading.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index ace525c60088..eb435bd8e5f4 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -261,7 +261,7 @@

@if (activeTab === 'test-analysis') { - + }
} From ce673abec8f375e459a3d76bf587fa9ff876e6fe Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 16 Aug 2024 23:27:25 +0200 Subject: [PATCH 14/52] small adjustment to failing test --- .../testcase-analysis/testcase-analysis.component.ts | 6 ++++-- .../artemis/assessment/ResultServiceIntegrationTest.java | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 538e3b74a154..7da0e5b69e2d 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -42,9 +42,11 @@ export class TestcaseAnalysisComponent implements OnInit { loadFeedbackDetails(exerciseId: number): void { this.resultService.getFeedbackDetailsForExercise(exerciseId).subscribe((response) => { - const feedbackArray = response.body?.feedback ?? []; this.participation = response.body?.participation ?? []; - + this.participation = this.participation.filter((participation) => { + return participation.results && participation.results.length > 0; + }); + const feedbackArray = response.body?.feedback ?? []; const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); this.saveFeedback(negativeFeedbackArray); }); diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index aa76a67f8403..7db78f878119 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -736,7 +736,7 @@ void testGetAssessmentCountByCorrectionRoundForProgrammingExercise() { @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") void testGetAllFeedbackDetailsForExercise() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); - StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, "student1"); + StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); Result result = participationUtilService.addResultToParticipation(null, null, participation); participationUtilService.addSampleFeedbackToResults(result); @@ -751,7 +751,7 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") void testGetAllFeedbackDetailsForExercise_NoFeedback() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); - StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, "student1"); + StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); ResultResource.FeedbackDetailsResponse response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, ResultResource.FeedbackDetailsResponse.class); From 6d32b025abdf1f0f21edbec8ec641dce5b7b388c Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 16 Aug 2024 23:47:14 +0200 Subject: [PATCH 15/52] small adjustment to failing test --- .../testcase-analysis/testcase-analysis.component.ts | 6 +++--- .../testcase-analysis/testcase-analysis.component.spec.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 7da0e5b69e2d..de1ce5e23622 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -42,11 +42,11 @@ export class TestcaseAnalysisComponent implements OnInit { loadFeedbackDetails(exerciseId: number): void { this.resultService.getFeedbackDetailsForExercise(exerciseId).subscribe((response) => { - this.participation = response.body?.participation ?? []; + this.participation = response.body?.participation || []; this.participation = this.participation.filter((participation) => { return participation.results && participation.results.length > 0; }); - const feedbackArray = response.body?.feedback ?? []; + const feedbackArray = response.body?.feedback || []; const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); this.saveFeedback(negativeFeedbackArray); }); @@ -80,7 +80,7 @@ export class TestcaseAnalysisComponent implements OnInit { findTaskIndexForTestCase(testCase?: ProgrammingExerciseTestCase): number { if (!testCase) { - return 0; + return -1; } return this.tasks.findIndex((task) => task.testCases.some((tc) => tc.testName === testCase.testName)) + 1; } diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 6db934ab0bea..493b0d114fc0 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -108,7 +108,7 @@ describe('TestcaseAnalysisComponent', () => { expect(zeroIndex).toBe(0); const undefinedIndex = component.findTaskIndexForTestCase(undefined); - expect(undefinedIndex).toBe(0); + expect(undefinedIndex).toBe(-1); }); it('should handle errors when loading feedback details', () => { From 737ea02838785ce69519cfc67f69a22f950fef38 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 16 Aug 2024 23:49:30 +0200 Subject: [PATCH 16/52] small adjustment to failing test --- .../artemis/assessment/ResultServiceIntegrationTest.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index 7db78f878119..999961594829 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -60,7 +60,6 @@ import de.tum.in.www1.artemis.participation.ParticipationFactory; import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.ExamRepository; -import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.repository.FeedbackRepository; import de.tum.in.www1.artemis.repository.FileUploadExerciseRepository; import de.tum.in.www1.artemis.repository.GradingCriterionRepository; @@ -72,7 +71,6 @@ import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.repository.TextExerciseRepository; -import de.tum.in.www1.artemis.service.ResultService; import de.tum.in.www1.artemis.web.rest.ResultResource; import de.tum.in.www1.artemis.web.rest.dto.ResultWithPointsPerGradingCriterionDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -129,12 +127,6 @@ class ResultServiceIntegrationTest extends AbstractSpringIntegrationLocalCILocal @Autowired private ExamUtilService examUtilService; - @Autowired - private ExerciseRepository exerciseRepository; - - @Autowired - private ResultService resultService; - private Course course; private ProgrammingExercise programmingExercise; From f2562481bf89279d1506fb5e81cc73ec11b0f69c Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sat, 17 Aug 2024 13:04:11 +0200 Subject: [PATCH 17/52] adjusted performance even more and added query --- .../artemis/repository/ResultRepository.java | 20 ++++++++++ .../www1/artemis/web/rest/ResultResource.java | 40 +++++++------------ .../testcase-analysis.component.html | 2 +- .../testcase-analysis.component.ts | 6 +-- .../exercises/shared/result/result.service.ts | 2 +- .../ResultServiceIntegrationTest.java | 6 +-- .../testcase-analysis.component.spec.ts | 4 +- 7 files changed, 45 insertions(+), 35 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java index d0a20634642a..e9b3a26069ef 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java @@ -198,6 +198,15 @@ default Optional findFirstByParticipationIdAndRatedWithSubmissionOrderBy """) Optional findByIdWithEagerFeedbacks(@Param("resultId") long resultId); + @Query(""" + SELECT r + FROM Result r + LEFT JOIN FETCH r.feedbacks f + LEFT JOIN FETCH f.testCase + WHERE r.id IN :resultIds + """) + List findAllByIdWithEagerFeedbacks(@Param("resultIds") List resultIds); + @Query(""" SELECT r FROM Result r @@ -838,6 +847,17 @@ default Result findByIdWithEagerFeedbacksElseThrow(long resultId) { return getValueElseThrow(findByIdWithEagerFeedbacks(resultId), resultId); } + /** + * Get the results with the given IDs from the database. The results are loaded together with their feedbacks. + * Throws an EntityNotFoundException if no results could be found for any of the given IDs. + * + * @param resultIds the list of ids for the results that should be loaded from the database + * @return the list of results with the given ids + */ + default List findAllByIdWithEagerFeedbacksElseThrow(List resultIds) { + return getArbitraryValueElseThrow(Optional.of(findAllByIdWithEagerFeedbacks(resultIds)), "Results with IDs: " + resultIds); + } + /** * Given the example submission list, it returns the results of the linked submission, if any * diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index fb3b9ed41c94..9ee742f111f9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -5,12 +5,13 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.ZonedDateTime; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -279,12 +280,6 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, savedResult.getId().toString())).body(savedResult); } - /** - * GET /exercises/:exerciseId/feedback-details : get all feedback details and participations for an exercise. - * - * @param exerciseId The ID of the exercise - * @return A response entity containing feedback details and participations - */ @GetMapping("exercises/{exerciseId}/feedback-details") @EnforceAtLeastTutor public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { @@ -292,30 +287,25 @@ public ResponseEntity getAllFeedbackDetailsForExercise( Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); // Get all Student participations for the exercise - Set participation; + Set participations; if (exercise.isTeamMode()) { - participation = studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsWithTeamInformation(exercise.getId()); + participations = studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsWithTeamInformation(exercise.getId()); } else { - participation = studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsAndAssessmentNote(exercise.getId()); + participations = studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsAndAssessmentNote(exercise.getId()); } - removeSubmissionAndExerciseData(participation); + removeSubmissionAndExerciseData(participations); - List allFeedback = new ArrayList<>(); + // Gather result IDs from the latest result of each participation + List resultIds = participations.stream() + .map(singleParticipation -> singleParticipation.getResults().stream().max(Comparator.comparing(Result::getCompletionDate)).map(Result::getId).orElse(null)) + .filter(Objects::nonNull).collect(Collectors.toList()); - // For each participation, get the feedback from its latest result - for (StudentParticipation singleParticipation : participation) { - Result latestResult = singleParticipation.getResults().stream().max(Comparator.comparing(Result::getCompletionDate)).orElse(null); - - if (latestResult != null) { - List feedback = getResultDetails(singleParticipation.getId(), latestResult.getId()).getBody(); - if (feedback != null) { - allFeedback.addAll(feedback); - } - } - } + // Fetch all feedbacks in one query using the list of result IDs + List allFeedback = resultRepository.findAllByIdWithEagerFeedbacksElseThrow(resultIds).stream().flatMap(result -> result.getFeedbacks().stream()) + .collect(Collectors.toList()); - FeedbackDetailsResponse response = new FeedbackDetailsResponse(allFeedback, participation); + FeedbackDetailsResponse response = new FeedbackDetailsResponse(allFeedback, participations); return new ResponseEntity<>(response, HttpStatus.OK); } @@ -330,7 +320,7 @@ private void removeSubmissionAndExerciseData(Set participa }); } - public record FeedbackDetailsResponse(List feedback, Set participation) { + public record FeedbackDetailsResponse(List feedback, Set participations) { } } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html index 5dc1f7748e3b..508356350660 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html @@ -13,7 +13,7 @@

@for (item of feedback; track item) { - {{ item.count }} ({{ (this.participation.length > 0 ? item.count / this.participation.length : 0) * 100 | number: '1.0-0' }}%) + {{ item.count }} ({{ (this.participations.length > 0 ? item.count / this.participations.length : 0) * 100 | number: '1.0-0' }}%) {{ item.detailText }} {{ item.task }} {{ item.testcase }} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index de1ce5e23622..4e30ebdd7d5c 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -23,7 +23,7 @@ type FeedbackDetail = { }) export class TestcaseAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; - participation: Participation[] = []; + participations: Participation[] = []; tasks: ProgrammingExerciseTask[] = []; feedback: FeedbackDetail[] = []; @@ -42,8 +42,8 @@ export class TestcaseAnalysisComponent implements OnInit { loadFeedbackDetails(exerciseId: number): void { this.resultService.getFeedbackDetailsForExercise(exerciseId).subscribe((response) => { - this.participation = response.body?.participation || []; - this.participation = this.participation.filter((participation) => { + this.participations = response.body?.participations || []; + this.participations = this.participations.filter((participation) => { return participation.results && participation.results.length > 0; }); const feedbackArray = response.body?.feedback || []; diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index a01cdf9dc7d9..3eb60a02bd29 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -48,7 +48,7 @@ export interface IResultService { interface FeedbackDetailsResponse { feedback: Feedback[]; - participation: Participation[]; + participations: Participation[]; } @Injectable({ providedIn: 'root' }) diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index 999961594829..2382ef5f513e 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -736,7 +736,7 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { ResultResource.FeedbackDetailsResponse.class); assertThat(response.feedback()).isNotEmpty(); - assertThat(response.participation()).hasSize(1); + assertThat(response.participations()).hasSize(1); } @Test @@ -749,7 +749,7 @@ void testGetAllFeedbackDetailsForExercise_NoFeedback() throws Exception { ResultResource.FeedbackDetailsResponse.class); assertThat(response.feedback()).isEmpty(); - assertThat(response.participation()).hasSize(1); + assertThat(response.participations()).hasSize(1); } @Test @@ -761,7 +761,7 @@ void testGetAllFeedbackDetailsForExercise_NoParticipations() throws Exception { ResultResource.FeedbackDetailsResponse.class); assertThat(response.feedback()).isEmpty(); - assertThat(response.participation()).isEmpty(); + assertThat(response.participations()).isEmpty(); } @Test diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 493b0d114fc0..2700eaeebe77 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -53,7 +53,7 @@ describe('TestcaseAnalysisComponent', () => { ] as ProgrammingExerciseTask[]; const feedbackDetailsResponseMock = new HttpResponse({ - body: { feedback: feedbackMock, participation: participationMock }, + body: { feedback: feedbackMock, participations: participationMock }, }); beforeEach(() => { @@ -84,7 +84,7 @@ describe('TestcaseAnalysisComponent', () => { fixture.detectChanges(); expect(resultService.getFeedbackDetailsForExercise).toHaveBeenCalled(); - expect(component.participation).toEqual(participationMock); + expect(component.participations).toEqual(participationMock); expect(component.feedback).toHaveLength(2); expect(component.feedback[0].detailText).toBe('Test feedback 1 detail'); expect(component.feedback[1].detailText).toBe('Test feedback 2 detail'); From fc9ea875127022b5cc93fc2981de600d3958c0e1 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sat, 17 Aug 2024 21:17:45 +0200 Subject: [PATCH 18/52] adjusted performance even more and implemented feedback --- .../artemis/repository/ResultRepository.java | 22 ++++++++--- .../www1/artemis/web/rest/ResultResource.java | 38 +++++++------------ ...-exercise-configure-grading.component.html | 6 +-- ...ng-exercise-configure-grading.component.ts | 2 +- .../testcase-analysis.component.html | 2 +- .../testcase-analysis.component.scss | 1 - .../testcase-analysis.component.ts | 8 +--- .../exercises/shared/result/result.service.ts | 2 +- .../webapp/i18n/de/programmingExercise.json | 12 +++--- .../webapp/i18n/en/programmingExercise.json | 2 +- .../ResultServiceIntegrationTest.java | 15 ++------ .../testcase-analysis.component.spec.ts | 12 +----- 12 files changed, 51 insertions(+), 71 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java index e9b3a26069ef..8ce5d6a92a05 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java @@ -34,6 +34,7 @@ import de.tum.in.www1.artemis.domain.assessment.dashboard.ResultCount; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.leaderboard.tutor.TutorLeaderboardAssessments; +import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; import de.tum.in.www1.artemis.service.util.RoundingUtil; import de.tum.in.www1.artemis.web.rest.dto.DueDateStat; @@ -199,13 +200,22 @@ default Optional findFirstByParticipationIdAndRatedWithSubmissionOrderBy Optional findByIdWithEagerFeedbacks(@Param("resultId") long resultId); @Query(""" - SELECT r - FROM Result r + SELECT DISTINCT p + FROM StudentParticipation p + LEFT JOIN FETCH p.results r + LEFT JOIN FETCH r.submission s + LEFT JOIN FETCH p.submissions + LEFT JOIN FETCH r.assessmentNote LEFT JOIN FETCH r.feedbacks f LEFT JOIN FETCH f.testCase - WHERE r.id IN :resultIds + WHERE p.exercise.id = :exerciseId + AND ( + r.id = (SELECT MAX(p_r.id) FROM p.results p_r) + OR r.assessmentType <> de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC + OR r IS NULL + ) """) - List findAllByIdWithEagerFeedbacks(@Param("resultIds") List resultIds); + Set findByExerciseIdWithLatestResultAndFeedback(@Param("exerciseId") long exerciseId); @Query(""" SELECT r @@ -854,8 +864,8 @@ default Result findByIdWithEagerFeedbacksElseThrow(long resultId) { * @param resultIds the list of ids for the results that should be loaded from the database * @return the list of results with the given ids */ - default List findAllByIdWithEagerFeedbacksElseThrow(List resultIds) { - return getArbitraryValueElseThrow(Optional.of(findAllByIdWithEagerFeedbacks(resultIds)), "Results with IDs: " + resultIds); + default Set findByExerciseIdWithLatestResultAndFeedbackElseThrow(long exerciseId) { + return getArbitraryValueElseThrow(Optional.of(findByExerciseIdWithLatestResultAndFeedback(exerciseId)), "Results with Feedback for: " + exerciseId); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 9ee742f111f9..3d5d01a66de0 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -5,13 +5,11 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.ZonedDateTime; -import java.util.Comparator; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -284,43 +282,35 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo @EnforceAtLeastTutor public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); - Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); - // Get all Student participations for the exercise - Set participations; - if (exercise.isTeamMode()) { - participations = studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsWithTeamInformation(exercise.getId()); - } - else { - participations = studentParticipationRepository.findByExerciseIdWithLatestAndManualResultsAndAssessmentNote(exercise.getId()); - } + // Fetch participations along with the latest results and their feedbacks + Set participations = resultRepository.findByExerciseIdWithLatestResultAndFeedbackElseThrow(exerciseId); removeSubmissionAndExerciseData(participations); - // Gather result IDs from the latest result of each participation - List resultIds = participations.stream() - .map(singleParticipation -> singleParticipation.getResults().stream().max(Comparator.comparing(Result::getCompletionDate)).map(Result::getId).orElse(null)) - .filter(Objects::nonNull).collect(Collectors.toList()); + // Collect all feedback and result IDs from the participations + List allFeedback = new ArrayList<>(); + List resultIds = new ArrayList<>(); - // Fetch all feedbacks in one query using the list of result IDs - List allFeedback = resultRepository.findAllByIdWithEagerFeedbacksElseThrow(resultIds).stream().flatMap(result -> result.getFeedbacks().stream()) - .collect(Collectors.toList()); + participations.forEach(participation -> { + participation.getResults().forEach(result -> { + resultIds.add(result.getId()); + allFeedback.addAll(result.getFeedbacks()); + }); + }); - FeedbackDetailsResponse response = new FeedbackDetailsResponse(allFeedback, participations); + FeedbackDetailsResponse response = new FeedbackDetailsResponse(allFeedback, resultIds); return new ResponseEntity<>(response, HttpStatus.OK); } private void removeSubmissionAndExerciseData(Set participations) { // remove unnecessary data to reduce response size participations.forEach(participation -> { - participation.setSubmissionCount(participation.getSubmissions().size()); participation.setSubmissions(null); - }); - participations.stream().filter(participation -> participation.getParticipant() != null).peek(participation -> { participation.setExercise(null); }); } - public record FeedbackDetailsResponse(List feedback, Set participations) { + public record FeedbackDetailsResponse(List feedback, List resultIds) { } } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index eb435bd8e5f4..f41298353830 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -16,7 +16,7 @@

-
+
@@ -32,7 +32,7 @@

} - @if (programmingExercise.isAtLeastInstructor && activeTab !== 'test-analysis') { + @if (programmingExercise.isAtLeastInstructor && activeTab !== 'testcase-analysis') { }
- @if (activeTab === 'test-analysis') { + @if (activeTab === 'testcase-analysis') { }
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts index a65fc0091b86..0fd57c0a33f1 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts @@ -56,7 +56,7 @@ const DefaultFieldValues: { [key: string]: number } = { [EditableField.MAX_PENALTY]: 0, }; -export type GradingTab = 'test-cases' | 'code-analysis' | 'submission-policy' | 'test-analysis'; +export type GradingTab = 'test-cases' | 'code-analysis' | 'submission-policy' | 'testcase-analysis'; export type Table = 'testCases' | 'codeAnalysis'; @Component({ diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html index 508356350660..2a1710616d37 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html @@ -13,7 +13,7 @@

@for (item of feedback; track item) { - {{ item.count }} ({{ (this.participations.length > 0 ? item.count / this.participations.length : 0) * 100 | number: '1.0-0' }}%) + {{ item.count }} ({{ (this.resultIds.length > 0 ? item.count / this.resultIds.length : 0) * 100 | number: '1.0-0' }}%) {{ item.detailText }} {{ item.task }} {{ item.testcase }} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss index a2737fdf3f38..e00ecf4e7965 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss @@ -1,4 +1,3 @@ .table { - width: 100%; margin-bottom: 1rem; } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 4e30ebdd7d5c..ed39670744fa 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -4,7 +4,6 @@ import { ResultService } from 'app/exercises/shared/result/result.service'; import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service'; import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task'; import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; -import { Participation } from 'app/entities/participation/participation.model'; import { ArtemisSharedModule } from 'app/shared/shared.module'; type FeedbackDetail = { @@ -23,7 +22,7 @@ type FeedbackDetail = { }) export class TestcaseAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; - participations: Participation[] = []; + resultIds: number[] = []; tasks: ProgrammingExerciseTask[] = []; feedback: FeedbackDetail[] = []; @@ -42,10 +41,7 @@ export class TestcaseAnalysisComponent implements OnInit { loadFeedbackDetails(exerciseId: number): void { this.resultService.getFeedbackDetailsForExercise(exerciseId).subscribe((response) => { - this.participations = response.body?.participations || []; - this.participations = this.participations.filter((participation) => { - return participation.results && participation.results.length > 0; - }); + this.resultIds = response.body?.resultIds || []; const feedbackArray = response.body?.feedback || []; const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); this.saveFeedback(negativeFeedbackArray); diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index 3eb60a02bd29..a41f3df98ad9 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -48,7 +48,7 @@ export interface IResultService { interface FeedbackDetailsResponse { feedback: Feedback[]; - participations: Participation[]; + resultIds: number[]; } @Injectable({ providedIn: 'root' }) diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index a6e825d75e13..97518c438fb4 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -260,8 +260,8 @@ "settingNegative": "Der Testfall {{testCase}} darf keine Einstellungen mit negativen Werten haben." }, "categories": { - "titleHeader": "Code-Analyse", - "title": "Code-Analyse-Kategorien", + "titleHeader": "Quelltext-Analyse", + "title": "Quelltext-Analyse-Kategorien", "notGraded": "Nicht bewertet.", "noFeedback": "Ohne sichtbares Feedback.", "updated": "Die Kategorien wurden erfolgreich gespeichert.", @@ -309,13 +309,13 @@ }, "testAnalysis": { "titleHeader": "Test Analyse", - "title": "Error Analyse für {{exerciseTitle}}", + "title": "Fehler Analyse für {{exerciseTitle}}", "occurrence": "Häufigkeit", "testCaseFeedback": "Test Fall Feedback", - "task": "Task", + "task": "Aufgabe", "testcase": "Test Fall", - "errorCategory": "Error Kategorie", - "totalItems": "Insgesamt {{count}} items" + "errorCategory": "Fehler Kategorie", + "totalItems": "Insgesamt {{count}} stück" }, "help": { "name": "Aufgabennamen werden fett geschrieben, während Testnamen normal sind. Ob es ein Aufgabenname oder Testname ist hängt davon ab, ob die Reihe eine Aufgabe oder einen Test darstellt.", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 7ed52b0d69d0..edf01f60479d 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -317,7 +317,7 @@ "task": "Task", "testcase": "Testcase", "errorCategory": "Error Category", - "totalItems": "Total {{count}} items" + "totalItems": "In total {{count}} items" }, "help": { "name": "Task names are written in bold whereas Test names are normal. Task or test name depending on whether the row is a task or test.", diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index 2382ef5f513e..6ef6297bd444 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -736,7 +736,7 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { ResultResource.FeedbackDetailsResponse.class); assertThat(response.feedback()).isNotEmpty(); - assertThat(response.participations()).hasSize(1); + assertThat(response.resultIds()).containsExactly(result.getId()); } @Test @@ -744,12 +744,13 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { void testGetAllFeedbackDetailsForExercise_NoFeedback() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); + Result result = participationUtilService.addResultToParticipation(null, null, participation); ResultResource.FeedbackDetailsResponse response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, ResultResource.FeedbackDetailsResponse.class); assertThat(response.feedback()).isEmpty(); - assertThat(response.participations()).hasSize(1); + assertThat(response.resultIds()).containsExactly(result.getId()); } @Test @@ -761,15 +762,7 @@ void testGetAllFeedbackDetailsForExercise_NoParticipations() throws Exception { ResultResource.FeedbackDetailsResponse.class); assertThat(response.feedback()).isEmpty(); - assertThat(response.participations()).isEmpty(); - } - - @Test - @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") - void testGetAllFeedbackDetailsForExercise_InvalidExerciseId() throws Exception { - long invalidExerciseId = 9999L; - - request.get("/api/exercises/" + invalidExerciseId + "/feedback-details", HttpStatus.NOT_FOUND, ResultResource.FeedbackDetailsResponse.class); + assertThat(response.resultIds()).isEmpty(); } } diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 2700eaeebe77..7806d05a35f8 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -6,7 +6,6 @@ import { ArtemisTestModule } from '../../../test.module'; import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component'; import { ResultService } from 'app/exercises/shared/result/result.service'; import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service'; -import { Participation } from 'app/entities/participation/participation.model'; import { Feedback } from 'app/entities/feedback.model'; import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task'; import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; @@ -19,13 +18,6 @@ describe('TestcaseAnalysisComponent', () => { let fixture: ComponentFixture; let resultService: ResultService; - const participationMock: Participation[] = [ - { - id: 1, - results: [{ id: 1 }], - }, - ] as Participation[]; - const feedbackMock: Feedback[] = [ { text: 'Test feedback 1', @@ -53,7 +45,7 @@ describe('TestcaseAnalysisComponent', () => { ] as ProgrammingExerciseTask[]; const feedbackDetailsResponseMock = new HttpResponse({ - body: { feedback: feedbackMock, participations: participationMock }, + body: { feedback: feedbackMock, resultIds: [1, 2] }, }); beforeEach(() => { @@ -84,7 +76,7 @@ describe('TestcaseAnalysisComponent', () => { fixture.detectChanges(); expect(resultService.getFeedbackDetailsForExercise).toHaveBeenCalled(); - expect(component.participations).toEqual(participationMock); + expect(component.resultIds).toEqual([1, 2]); expect(component.feedback).toHaveLength(2); expect(component.feedback[0].detailText).toBe('Test feedback 1 detail'); expect(component.feedback[1].detailText).toBe('Test feedback 2 detail'); From 6bf690bc36449c112482a7241f24dc2252f055aa Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sat, 17 Aug 2024 21:26:08 +0200 Subject: [PATCH 19/52] scss removed for now --- .../grading/testcase-analysis/testcase-analysis.component.html | 2 +- .../grading/testcase-analysis/testcase-analysis.component.scss | 3 --- .../grading/testcase-analysis/testcase-analysis.component.ts | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html index 2a1710616d37..b1a237421357 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html @@ -1,6 +1,6 @@

- +
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss deleted file mode 100644 index e00ecf4e7965..000000000000 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.table { - margin-bottom: 1rem; -} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index ed39670744fa..0cbd4b8a83fa 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -16,7 +16,6 @@ type FeedbackDetail = { @Component({ selector: 'jhi-testcase-analysis', templateUrl: './testcase-analysis.component.html', - styleUrls: ['./testcase-analysis.component.scss'], standalone: true, imports: [ArtemisSharedModule], }) From 231ed5df27f20848d3e18ad4bd223082b7442990 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sat, 17 Aug 2024 21:30:05 +0200 Subject: [PATCH 20/52] coderabbit --- .../testcase-analysis.component.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 0cbd4b8a83fa..9e015732818e 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -32,7 +32,7 @@ export class TestcaseAnalysisComponent implements OnInit { ngOnInit(): void { const exerciseId = this.programmingExerciseTaskService.exercise?.id; - if (exerciseId !== undefined) { + if (exerciseId) { this.loadFeedbackDetails(exerciseId); } this.tasks = this.programmingExerciseTaskService.updateTasks(); @@ -55,17 +55,15 @@ export class TestcaseAnalysisComponent implements OnInit { const testcase = feedback.testCase?.testName ?? ''; const key = `${feedbackText}_${testcase}`; - if (feedbackMap.has(key)) { - const existingFeedback = feedbackMap.get(key); - if (existingFeedback) { - existingFeedback.count += 1; - } + const existingFeedback = feedbackMap.get(key); + if (existingFeedback) { + existingFeedback.count += 1; } else { const task = this.findTaskIndexForTestCase(feedback.testCase); - feedbackMap.set(key, { + feedbackMap.set(key, { count: 1, detailText: feedback.detailText ?? '', - testcase: feedback.testCase?.testName, + testcase: feedback.testCase?.testName ?? '', task: task, }); } From 957b592ef37de8c7573d6e571d3b32f9779ad0c3 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sat, 17 Aug 2024 21:39:58 +0200 Subject: [PATCH 21/52] server style --- .../tum/in/www1/artemis/repository/ResultRepository.java | 9 +++++---- .../de/tum/in/www1/artemis/web/rest/ResultResource.java | 8 ++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java index 8ce5d6a92a05..d2e59853bcaf 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java @@ -858,11 +858,12 @@ default Result findByIdWithEagerFeedbacksElseThrow(long resultId) { } /** - * Get the results with the given IDs from the database. The results are loaded together with their feedbacks. - * Throws an EntityNotFoundException if no results could be found for any of the given IDs. + * Get the latest results and their feedbacks for a given exercise ID from the database. + * The results are loaded together with their feedbacks. + * Throws an EntityNotFoundException if no results could be found for the given exercise ID. * - * @param resultIds the list of ids for the results that should be loaded from the database - * @return the list of results with the given ids + * @param exerciseId the ID of the exercise for which the latest results and feedbacks should be loaded from the database + * @return the set of student participations containing the latest results and their feedbacks */ default Set findByExerciseIdWithLatestResultAndFeedbackElseThrow(long exerciseId) { return getArbitraryValueElseThrow(Optional.of(findByExerciseIdWithLatestResultAndFeedback(exerciseId)), "Results with Feedback for: " + exerciseId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 3d5d01a66de0..8def3a17004d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -278,16 +278,20 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, savedResult.getId().toString())).body(savedResult); } + /** + * GET exercises/:exerciseId/feedback-details : Retrieves all feedback details and the latest result IDs for a given exercise. + * + * @param exerciseId The ID of the exercise for which feedback details and result IDs should be retrieved. + * @return A ResponseEntity containing a list of all feedback details and the corresponding result IDs for the exercise. + */ @GetMapping("exercises/{exerciseId}/feedback-details") @EnforceAtLeastTutor public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); - // Fetch participations along with the latest results and their feedbacks Set participations = resultRepository.findByExerciseIdWithLatestResultAndFeedbackElseThrow(exerciseId); removeSubmissionAndExerciseData(participations); - // Collect all feedback and result IDs from the participations List allFeedback = new ArrayList<>(); List resultIds = new ArrayList<>(); From 016336b41a9a402d6bf7af249016aea99e627188 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 18 Aug 2024 14:25:58 +0200 Subject: [PATCH 22/52] optimized again --- .../artemis/repository/ResultRepository.java | 31 ------ .../StudentParticipationRepository.java | 30 +++++ .../www1/artemis/web/rest/ResultResource.java | 24 ++-- .../rest/dto/feedback/FeedbackDetailDTO.java | 4 + .../FeedbackDetailsWithResultIdsDTO.java | 6 + ...-exercise-configure-grading.component.html | 2 +- ...ng-exercise-configure-grading.component.ts | 2 +- .../testcase-analysis.component.ts | 60 +++++----- .../testcase-analysis.service.ts | 28 +++++ .../exercises/shared/result/result.service.ts | 8 +- .../ResultServiceIntegrationTest.java | 32 ++++-- .../testcase-analysis.component.spec.ts | 104 ++++++++---------- 12 files changed, 189 insertions(+), 142 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java index d2e59853bcaf..d0a20634642a 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ResultRepository.java @@ -34,7 +34,6 @@ import de.tum.in.www1.artemis.domain.assessment.dashboard.ResultCount; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.leaderboard.tutor.TutorLeaderboardAssessments; -import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; import de.tum.in.www1.artemis.service.util.RoundingUtil; import de.tum.in.www1.artemis.web.rest.dto.DueDateStat; @@ -199,24 +198,6 @@ default Optional findFirstByParticipationIdAndRatedWithSubmissionOrderBy """) Optional findByIdWithEagerFeedbacks(@Param("resultId") long resultId); - @Query(""" - SELECT DISTINCT p - FROM StudentParticipation p - LEFT JOIN FETCH p.results r - LEFT JOIN FETCH r.submission s - LEFT JOIN FETCH p.submissions - LEFT JOIN FETCH r.assessmentNote - LEFT JOIN FETCH r.feedbacks f - LEFT JOIN FETCH f.testCase - WHERE p.exercise.id = :exerciseId - AND ( - r.id = (SELECT MAX(p_r.id) FROM p.results p_r) - OR r.assessmentType <> de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC - OR r IS NULL - ) - """) - Set findByExerciseIdWithLatestResultAndFeedback(@Param("exerciseId") long exerciseId); - @Query(""" SELECT r FROM Result r @@ -857,18 +838,6 @@ default Result findByIdWithEagerFeedbacksElseThrow(long resultId) { return getValueElseThrow(findByIdWithEagerFeedbacks(resultId), resultId); } - /** - * Get the latest results and their feedbacks for a given exercise ID from the database. - * The results are loaded together with their feedbacks. - * Throws an EntityNotFoundException if no results could be found for the given exercise ID. - * - * @param exerciseId the ID of the exercise for which the latest results and feedbacks should be loaded from the database - * @return the set of student participations containing the latest results and their feedbacks - */ - default Set findByExerciseIdWithLatestResultAndFeedbackElseThrow(long exerciseId) { - return getArbitraryValueElseThrow(Optional.of(findByExerciseIdWithLatestResultAndFeedback(exerciseId)), "Results with Feedback for: " + exerciseId); - } - /** * Given the example submission list, it returns the results of the linked submission, if any * diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 8cd15a15b573..f2cadba7f43c 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -1210,4 +1210,34 @@ SELECT COALESCE(AVG(p.presentationScore), 0) AND p.presentationScore IS NOT NULL """) double getAvgPresentationScoreByCourseId(@Param("courseId") long courseId); + + @Query(""" + SELECT DISTINCT p + FROM StudentParticipation p + LEFT JOIN FETCH p.results r + LEFT JOIN FETCH r.submission s + LEFT JOIN FETCH r.assessmentNote + LEFT JOIN FETCH r.feedbacks f + LEFT JOIN FETCH f.testCase + WHERE p.exercise.id = :exerciseId + AND ( + r.id = (SELECT MAX(p_r.id) FROM p.results p_r) + OR r.assessmentType <> de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC + OR r IS NULL + ) + """) + Set findByExerciseIdWithLatestResultAndFeedbackAndTestcases(@Param("exerciseId") long exerciseId); + + /** + * Get the latest results and their feedbacks for a given exercise ID from the database (independant of if the correction was manual or automatic). + * The results are loaded together with their feedback and testcases. + * Throws an EntityNotFoundException if no results could be found for the given exercise ID. + * + * @param exerciseId the ID of the exercise for which the latest results and feedbacks should be loaded from the database + * @return the set of student participations containing the latest results and their feedbacks + */ + default Set findByExerciseIdWithLatestResultAndFeedbackAndTestcasesElseThrow(long exerciseId) { + return getArbitraryValueElseThrow(Optional.of(findByExerciseIdWithLatestResultAndFeedbackAndTestcases(exerciseId)), "Results with Feedback for: " + exerciseId); + } + } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 8def3a17004d..11e1c5ca7f00 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -52,6 +52,8 @@ import de.tum.in.www1.artemis.service.ResultService; import de.tum.in.www1.artemis.service.exam.ExamDateService; import de.tum.in.www1.artemis.web.rest.dto.ResultWithPointsPerGradingCriterionDTO; +import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailDTO; +import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailsWithResultIdsDTO; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; @@ -279,30 +281,37 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo } /** - * GET exercises/:exerciseId/feedback-details : Retrieves all feedback details and the latest result IDs for a given exercise. + * GET exercises/:exerciseId/feedback-details : Retrieves all negative feedback details and the latest result IDs for a given exercise. * * @param exerciseId The ID of the exercise for which feedback details and result IDs should be retrieved. * @return A ResponseEntity containing a list of all feedback details and the corresponding result IDs for the exercise. */ @GetMapping("exercises/{exerciseId}/feedback-details") @EnforceAtLeastTutor - public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { + public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); - Set participations = resultRepository.findByExerciseIdWithLatestResultAndFeedbackElseThrow(exerciseId); + Set participations = studentParticipationRepository.findByExerciseIdWithLatestResultAndFeedbackAndTestcasesElseThrow(exerciseId); removeSubmissionAndExerciseData(participations); - List allFeedback = new ArrayList<>(); + List allFeedbackDetails = new ArrayList<>(); List resultIds = new ArrayList<>(); participations.forEach(participation -> { participation.getResults().forEach(result -> { resultIds.add(result.getId()); - allFeedback.addAll(result.getFeedbacks()); + result.getFeedbacks().forEach(feedback -> { + if (Boolean.FALSE.equals(feedback.isPositive())) { + String detailText = feedback.getDetailText(); + String testCaseName = feedback.getTestCase() != null ? feedback.getTestCase().getTestName() : null; + FeedbackDetailDTO feedbackDetailDTO = new FeedbackDetailDTO(detailText, testCaseName); + allFeedbackDetails.add(feedbackDetailDTO); + } + }); }); }); - FeedbackDetailsResponse response = new FeedbackDetailsResponse(allFeedback, resultIds); + FeedbackDetailsWithResultIdsDTO response = new FeedbackDetailsWithResultIdsDTO(allFeedbackDetails, resultIds); return new ResponseEntity<>(response, HttpStatus.OK); } @@ -314,7 +323,4 @@ private void removeSubmissionAndExerciseData(Set participa }); } - public record FeedbackDetailsResponse(List feedback, List resultIds) { - } - } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java new file mode 100644 index 000000000000..86b8ccc5ea69 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java @@ -0,0 +1,4 @@ +package de.tum.in.www1.artemis.web.rest.dto.feedback; + +public record FeedbackDetailDTO(String detailText, String testCaseName) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java new file mode 100644 index 000000000000..63748a8dc6e6 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java @@ -0,0 +1,6 @@ +package de.tum.in.www1.artemis.web.rest.dto.feedback; + +import java.util.List; + +public record FeedbackDetailsWithResultIdsDTO(List feedbackDetails, List resultIds) { +} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index f41298353830..858e6643fc26 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -261,7 +261,7 @@

@if (activeTab === 'testcase-analysis') { - + }
} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts index 0fd57c0a33f1..c6e93e45d271 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts @@ -231,7 +231,7 @@ export class ProgrammingExerciseConfigureGradingComponent implements OnInit, OnD this.isLoading = false; } - if (params['tab'] === 'test-cases' || params['tab'] === 'code-analysis' || params['tab'] === 'submission-policy') { + if (params['tab'] === 'test-cases' || params['tab'] === 'code-analysis' || params['tab'] === 'submission-policy' || params['tab'] === 'testcase-analysis') { this.selectTab(params['tab']); } else { this.selectTab('test-cases'); diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 9e015732818e..40cfcbbb3ccc 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -1,10 +1,9 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Feedback } from 'app/entities/feedback.model'; import { ResultService } from 'app/exercises/shared/result/result.service'; -import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service'; -import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task'; -import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; +import { concatMap } from 'rxjs/operators'; +import { tap } from 'rxjs/operators'; type FeedbackDetail = { count: number; @@ -18,52 +17,63 @@ type FeedbackDetail = { templateUrl: './testcase-analysis.component.html', standalone: true, imports: [ArtemisSharedModule], + providers: [TestcaseAnalysisService], }) export class TestcaseAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; + @Input() exerciseId?: number; resultIds: number[] = []; - tasks: ProgrammingExerciseTask[] = []; + tasks: SimplifiedTask[] = []; feedback: FeedbackDetail[] = []; constructor( private resultService: ResultService, - private programmingExerciseTaskService: ProgrammingExerciseTaskService, + private simplifiedProgrammingExerciseTaskService: TestcaseAnalysisService, ) {} ngOnInit(): void { - const exerciseId = this.programmingExerciseTaskService.exercise?.id; - if (exerciseId) { - this.loadFeedbackDetails(exerciseId); + if (this.exerciseId) { + this.loadTasks(this.exerciseId) + .pipe(concatMap(() => this.loadFeedbackDetails(this.exerciseId!))) + .subscribe(); } - this.tasks = this.programmingExerciseTaskService.updateTasks(); } - loadFeedbackDetails(exerciseId: number): void { - this.resultService.getFeedbackDetailsForExercise(exerciseId).subscribe((response) => { - this.resultIds = response.body?.resultIds || []; - const feedbackArray = response.body?.feedback || []; - const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); - this.saveFeedback(negativeFeedbackArray); - }); + private loadTasks(exerciseId: number) { + return this.simplifiedProgrammingExerciseTaskService.getSimplifiedTasks(exerciseId).pipe( + tap((tasks) => { + this.tasks = tasks; + }), + ); + } + + private loadFeedbackDetails(exerciseId: number) { + return this.resultService.getFeedbackDetailsForExercise(exerciseId).pipe( + tap((response) => { + this.resultIds = response.body?.resultIds || []; + const feedbackArray = response.body?.feedbackDetails || []; + this.saveFeedback(feedbackArray); + }), + ); } - saveFeedback(feedbackArray: Feedback[]): void { + private saveFeedback(feedbackArray: { detailText: string; testCaseName: string }[]): void { const feedbackMap: Map = new Map(); feedbackArray.forEach((feedback) => { const feedbackText = feedback.detailText ?? ''; - const testcase = feedback.testCase?.testName ?? ''; + const testcase = feedback.testCaseName ?? ''; const key = `${feedbackText}_${testcase}`; const existingFeedback = feedbackMap.get(key); if (existingFeedback) { existingFeedback.count += 1; } else { - const task = this.findTaskIndexForTestCase(feedback.testCase); + const task = this.findTaskIndexForTestCase(testcase); feedbackMap.set(key, { count: 1, - detailText: feedback.detailText ?? '', - testcase: feedback.testCase?.testName ?? '', + detailText: feedbackText, + testcase: testcase, task: task, }); } @@ -71,10 +81,10 @@ export class TestcaseAnalysisComponent implements OnInit { this.feedback = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); } - findTaskIndexForTestCase(testCase?: ProgrammingExerciseTestCase): number { - if (!testCase) { + private findTaskIndexForTestCase(testCaseName: string): number { + if (!testCaseName) { return -1; } - return this.tasks.findIndex((task) => task.testCases.some((tc) => tc.testName === testCase.testName)) + 1; + return this.tasks.findIndex((tasks) => tasks.testCases && tasks.testCases.some((tc) => tc.testName === testCaseName)) + 1; } } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts new file mode 100644 index 000000000000..51017de35695 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; + +export interface SimplifiedTask { + taskName: string; + testCases: ProgrammingExerciseServerSideTask['testCases']; +} + +@Injectable() +export class TestcaseAnalysisService { + public resourceUrl = 'api/programming-exercises'; + + constructor(private http: HttpClient) {} + + public getSimplifiedTasks(exerciseId: number): Observable { + return this.http.get(`${this.resourceUrl}/${exerciseId}/tasks-with-unassigned-test-cases`).pipe( + map((tasks) => + tasks.map((task) => ({ + taskName: task.taskName ?? '', + testCases: task.testCases ?? [], + })), + ), + ); + } +} diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index a41f3df98ad9..020b1bc71633 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -46,8 +46,8 @@ export interface IResultService { triggerDownloadCSV: (rows: string[], csvFileName: string) => void; } -interface FeedbackDetailsResponse { - feedback: Feedback[]; +export interface FeedbackDetailsWithResultIdsDTO { + feedbackDetails: { detailText: string; testCaseName: string }[]; resultIds: number[]; } @@ -223,8 +223,8 @@ export class ResultService implements IResultService { ); } - getFeedbackDetailsForExercise(exerciseId: number): Observable> { - return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); + getFeedbackDetailsForExercise(exerciseId: number): Observable> { + return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); } public convertResultDatesFromClient(result: Result): Result { diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index 6ef6297bd444..d45f1a5232cc 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -71,8 +71,9 @@ import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; import de.tum.in.www1.artemis.repository.TextExerciseRepository; -import de.tum.in.www1.artemis.web.rest.ResultResource; import de.tum.in.www1.artemis.web.rest.dto.ResultWithPointsPerGradingCriterionDTO; +import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailDTO; +import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailsWithResultIdsDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; class ResultServiceIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { @@ -730,13 +731,22 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); Result result = participationUtilService.addResultToParticipation(null, null, participation); - participationUtilService.addSampleFeedbackToResults(result); - ResultResource.FeedbackDetailsResponse response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, - ResultResource.FeedbackDetailsResponse.class); + Feedback feedback = new Feedback(); + feedback.setPositive(false); + feedback.setDetailText("Some feedback"); + feedback.setTestCase(null); + participationUtilService.addFeedbackToResult(feedback, result); + + FeedbackDetailsWithResultIdsDTO response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, + FeedbackDetailsWithResultIdsDTO.class); - assertThat(response.feedback()).isNotEmpty(); + assertThat(response.feedbackDetails()).isNotEmpty(); assertThat(response.resultIds()).containsExactly(result.getId()); + + FeedbackDetailDTO feedbackDetail = response.feedbackDetails().getFirst(); + assertThat(feedbackDetail.detailText()).isEqualTo("Some feedback"); + assertThat(feedbackDetail.testCaseName()).isNull(); } @Test @@ -746,10 +756,10 @@ void testGetAllFeedbackDetailsForExercise_NoFeedback() throws Exception { StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); Result result = participationUtilService.addResultToParticipation(null, null, participation); - ResultResource.FeedbackDetailsResponse response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, - ResultResource.FeedbackDetailsResponse.class); + FeedbackDetailsWithResultIdsDTO response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, + FeedbackDetailsWithResultIdsDTO.class); - assertThat(response.feedback()).isEmpty(); + assertThat(response.feedbackDetails()).isEmpty(); assertThat(response.resultIds()).containsExactly(result.getId()); } @@ -758,10 +768,10 @@ void testGetAllFeedbackDetailsForExercise_NoFeedback() throws Exception { void testGetAllFeedbackDetailsForExercise_NoParticipations() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); - ResultResource.FeedbackDetailsResponse response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, - ResultResource.FeedbackDetailsResponse.class); + FeedbackDetailsWithResultIdsDTO response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, + FeedbackDetailsWithResultIdsDTO.class); - assertThat(response.feedback()).isEmpty(); + assertThat(response.feedbackDetails()).isEmpty(); assertThat(response.resultIds()).isEmpty(); } diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 7806d05a35f8..e15c5312a7f7 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -5,83 +5,76 @@ import { MockTranslateService } from '../../../helpers/mocks/service/mock-transl import { ArtemisTestModule } from '../../../test.module'; import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component'; import { ResultService } from 'app/exercises/shared/result/result.service'; -import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service'; -import { Feedback } from 'app/entities/feedback.model'; -import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task'; -import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; -import { ButtonComponent } from 'app/shared/components/button.component'; -import { MockComponent } from 'ng-mocks'; +import { TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; import { HttpResponse } from '@angular/common/http'; describe('TestcaseAnalysisComponent', () => { let component: TestcaseAnalysisComponent; let fixture: ComponentFixture; - let resultService: ResultService; - - const feedbackMock: Feedback[] = [ - { - text: 'Test feedback 1', - positive: false, - detailText: 'Test feedback 1 detail', - testCase: { testName: 'test1' } as ProgrammingExerciseTestCase, - }, - { - text: 'Test feedback 2', - positive: false, - detailText: 'Test feedback 2 detail', - testCase: { testName: 'test2' } as ProgrammingExerciseTestCase, - }, - { - text: 'Test feedback 1', - positive: false, - detailText: 'Test feedback 1 detail', - testCase: { testName: 'test1' } as ProgrammingExerciseTestCase, - }, - ] as Feedback[]; - - const tasksMock: ProgrammingExerciseTask[] = [ - { id: 1, taskName: 'Task 1', testCases: [{ testName: 'test1' } as ProgrammingExerciseTestCase] }, - { id: 2, taskName: 'Task 2', testCases: [{ testName: 'test2' } as ProgrammingExerciseTestCase] }, - ] as ProgrammingExerciseTask[]; + + const feedbackMock = [ + { detailText: 'Test feedback 1 detail', testCaseName: 'test1' }, + { detailText: 'Test feedback 2 detail', testCaseName: 'test2' }, + { detailText: 'Test feedback 1 detail', testCaseName: 'test1' }, + ]; + + const tasksMock = [ + { taskName: 'Task 1', testCases: [{ testName: 'test1' }] }, + { taskName: 'Task 2', testCases: [{ testName: 'test2' }] }, + ]; const feedbackDetailsResponseMock = new HttpResponse({ - body: { feedback: feedbackMock, resultIds: [1, 2] }, + body: { feedbackDetails: feedbackMock, resultIds: [1, 2] }, }); beforeEach(() => { - const mockProgrammingExerciseTaskService = { - exercise: { id: 1 }, // Mock the exercise with an id - updateTasks: jest.fn().mockReturnValue(tasksMock), - }; - TestBed.configureTestingModule({ - imports: [ArtemisTestModule, TranslateModule.forRoot()], - declarations: [TestcaseAnalysisComponent, MockComponent(ButtonComponent)], - providers: [ - { provide: TranslateService, useClass: MockTranslateService }, - ResultService, - { provide: ProgrammingExerciseTaskService, useValue: mockProgrammingExerciseTaskService }, + imports: [ + ArtemisTestModule, + TranslateModule.forRoot(), + TestcaseAnalysisComponent, // Import the standalone component here ], + providers: [{ provide: TranslateService, useClass: MockTranslateService }, ResultService, TestcaseAnalysisService], }).compileComponents(); fixture = TestBed.createComponent(TestcaseAnalysisComponent); component = fixture.componentInstance; - resultService = TestBed.inject(ResultService); + component.exerciseId = 1; - jest.spyOn(resultService, 'getFeedbackDetailsForExercise').mockReturnValue(of(feedbackDetailsResponseMock)); + // Mock the methods within the component itself + jest.spyOn(component, 'loadTasks').mockReturnValue(of(tasksMock)); + jest.spyOn(component, 'loadFeedbackDetails').mockReturnValue(of(feedbackDetailsResponseMock)); }); + //TODO: Fix the following test + /* it('should initialize and load feedback details correctly', () => { component.ngOnInit(); fixture.detectChanges(); - expect(resultService.getFeedbackDetailsForExercise).toHaveBeenCalled(); + // Assertions to ensure loadTasks and loadFeedbackDetails were called + expect(component.loadTasks).toHaveBeenCalledWith(component.exerciseId); + expect(component.loadFeedbackDetails).toHaveBeenCalledWith(component.exerciseId); + + // Further assertions to check if the component's state is updated correctly expect(component.resultIds).toEqual([1, 2]); expect(component.feedback).toHaveLength(2); expect(component.feedback[0].detailText).toBe('Test feedback 1 detail'); expect(component.feedback[1].detailText).toBe('Test feedback 2 detail'); }); + */ + + it('should handle errors when loading feedback details', () => { + // Mock loadFeedbackDetails to simulate an error + jest.spyOn(component, 'loadFeedbackDetails').mockReturnValue(throwError('Error')); + + component.loadFeedbackDetails(1); + + expect(component.loadFeedbackDetails).toHaveBeenCalled(); + expect(component.feedback).toHaveLength(0); + }); + it('should save feedbacks and sort them by count', () => { component.saveFeedback(feedbackMock); @@ -93,22 +86,13 @@ describe('TestcaseAnalysisComponent', () => { it('should find task index for a given test case', () => { component.tasks = tasksMock; - const index = component.findTaskIndexForTestCase({ testName: 'test1' } as ProgrammingExerciseTestCase); + const index = component.findTaskIndexForTestCase('test1'); expect(index).toBe(1); - const zeroIndex = component.findTaskIndexForTestCase({ testName: 'test3' } as ProgrammingExerciseTestCase); + const zeroIndex = component.findTaskIndexForTestCase('test3'); expect(zeroIndex).toBe(0); - const undefinedIndex = component.findTaskIndexForTestCase(undefined); + const undefinedIndex = component.findTaskIndexForTestCase(''); expect(undefinedIndex).toBe(-1); }); - - it('should handle errors when loading feedback details', () => { - jest.spyOn(resultService, 'getFeedbackDetailsForExercise').mockReturnValue(throwError('Error')); - - component.loadFeedbackDetails(1); - - expect(resultService.getFeedbackDetailsForExercise).toHaveBeenCalled(); - expect(component.feedback).toHaveLength(0); - }); }); From c87c056d2e2b317612260d015beba7b2b69a3cda Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 18 Aug 2024 16:59:17 +0200 Subject: [PATCH 23/52] fixed client test --- .../testcase-analysis.component.ts | 8 +- .../testcase-analysis.service.ts | 10 +- .../exercises/shared/result/result.service.ts | 4 - .../testcase-analysis.component.spec.ts | 165 ++++++++++++------ 4 files changed, 121 insertions(+), 66 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 40cfcbbb3ccc..6d23eb27126e 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -1,5 +1,4 @@ import { Component, Input, OnInit } from '@angular/core'; -import { ResultService } from 'app/exercises/shared/result/result.service'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; import { concatMap } from 'rxjs/operators'; @@ -26,10 +25,7 @@ export class TestcaseAnalysisComponent implements OnInit { tasks: SimplifiedTask[] = []; feedback: FeedbackDetail[] = []; - constructor( - private resultService: ResultService, - private simplifiedProgrammingExerciseTaskService: TestcaseAnalysisService, - ) {} + constructor(private simplifiedProgrammingExerciseTaskService: TestcaseAnalysisService) {} ngOnInit(): void { if (this.exerciseId) { @@ -48,7 +44,7 @@ export class TestcaseAnalysisComponent implements OnInit { } private loadFeedbackDetails(exerciseId: number) { - return this.resultService.getFeedbackDetailsForExercise(exerciseId).pipe( + return this.simplifiedProgrammingExerciseTaskService.getFeedbackDetailsForExercise(exerciseId).pipe( tap((response) => { this.resultIds = response.body?.resultIds || []; const feedbackArray = response.body?.feedbackDetails || []; diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts index 51017de35695..efd17daf892d 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; +import { FeedbackDetailsWithResultIdsDTO } from 'app/exercises/shared/result/result.service'; export interface SimplifiedTask { taskName: string; @@ -11,10 +12,15 @@ export interface SimplifiedTask { @Injectable() export class TestcaseAnalysisService { - public resourceUrl = 'api/programming-exercises'; + private resourceUrl = 'api/programming-exercises'; + private exerciseResourceUrl = 'api/exercises'; constructor(private http: HttpClient) {} + getFeedbackDetailsForExercise(exerciseId: number): Observable> { + return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); + } + public getSimplifiedTasks(exerciseId: number): Observable { return this.http.get(`${this.resourceUrl}/${exerciseId}/tasks-with-unassigned-test-cases`).pipe( map((tasks) => diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index 020b1bc71633..32321b49d799 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -223,10 +223,6 @@ export class ResultService implements IResultService { ); } - getFeedbackDetailsForExercise(exerciseId: number): Observable> { - return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); - } - public convertResultDatesFromClient(result: Result): Result { return Object.assign({}, result, { completionDate: convertDateFromClient(result.completionDate), diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index e15c5312a7f7..77457d941760 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -4,13 +4,15 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../test.module'; import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component'; -import { ResultService } from 'app/exercises/shared/result/result.service'; import { TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; import { HttpResponse } from '@angular/common/http'; describe('TestcaseAnalysisComponent', () => { - let component: TestcaseAnalysisComponent; let fixture: ComponentFixture; + let component: TestcaseAnalysisComponent; + let testcaseAnalysisService: TestcaseAnalysisService; + let getSimplifiedTasksSpy: jest.SpyInstance; + let getFeedbackDetailsSpy: jest.SpyInstance; const feedbackMock = [ { detailText: 'Test feedback 1 detail', testCaseName: 'test1' }, @@ -28,71 +30,126 @@ describe('TestcaseAnalysisComponent', () => { }); beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - ArtemisTestModule, - TranslateModule.forRoot(), - TestcaseAnalysisComponent, // Import the standalone component here + return TestBed.configureTestingModule({ + imports: [ArtemisTestModule, TranslateModule.forRoot(), TestcaseAnalysisComponent], + providers: [ + { + provide: TranslateService, + useClass: MockTranslateService, + }, + TestcaseAnalysisService, ], - providers: [{ provide: TranslateService, useClass: MockTranslateService }, ResultService, TestcaseAnalysisService], - }).compileComponents(); - - fixture = TestBed.createComponent(TestcaseAnalysisComponent); - component = fixture.componentInstance; - component.exerciseId = 1; - - // Mock the methods within the component itself - jest.spyOn(component, 'loadTasks').mockReturnValue(of(tasksMock)); - jest.spyOn(component, 'loadFeedbackDetails').mockReturnValue(of(feedbackDetailsResponseMock)); + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(TestcaseAnalysisComponent); + component = fixture.componentInstance; + component.exerciseId = 1; + + testcaseAnalysisService = fixture.debugElement.injector.get(TestcaseAnalysisService); + + getSimplifiedTasksSpy = jest.spyOn(testcaseAnalysisService, 'getSimplifiedTasks').mockReturnValue(of(tasksMock)); + getFeedbackDetailsSpy = jest.spyOn(testcaseAnalysisService, 'getFeedbackDetailsForExercise').mockReturnValue(of(feedbackDetailsResponseMock)); + }); }); - //TODO: Fix the following test - /* - it('should initialize and load feedback details correctly', () => { - component.ngOnInit(); - fixture.detectChanges(); - - // Assertions to ensure loadTasks and loadFeedbackDetails were called - expect(component.loadTasks).toHaveBeenCalledWith(component.exerciseId); - expect(component.loadFeedbackDetails).toHaveBeenCalledWith(component.exerciseId); - - // Further assertions to check if the component's state is updated correctly - expect(component.resultIds).toEqual([1, 2]); - expect(component.feedback).toHaveLength(2); - expect(component.feedback[0].detailText).toBe('Test feedback 1 detail'); - expect(component.feedback[1].detailText).toBe('Test feedback 2 detail'); + describe('ngOnInit', () => { + it('should call loadTasks and loadFeedbackDetails when exerciseId is provided', () => { + component.ngOnInit(); + + return new Promise((resolve) => { + setTimeout(() => { + expect(getSimplifiedTasksSpy).toHaveBeenCalledWith(1); + expect(getFeedbackDetailsSpy).toHaveBeenCalledWith(1); + expect(component.tasks).toEqual(tasksMock); + expect(component.resultIds).toEqual([1, 2]); + resolve(); + }, 0); + }); + }); + + it('should not call loadTasks and loadFeedbackDetails if exerciseId is not provided', () => { + component.exerciseId = undefined; + component.ngOnInit(); + + expect(getSimplifiedTasksSpy).not.toHaveBeenCalled(); + expect(getFeedbackDetailsSpy).not.toHaveBeenCalled(); + }); }); - */ + describe('loadTasks', () => { + it('should load tasks and update the component state', () => { + return component + .loadTasks(1) + .toPromise() + .then(() => { + expect(component.tasks).toEqual(tasksMock); + }); + }); + + it('should handle error while loading tasks', () => { + getSimplifiedTasksSpy.mockReturnValue(throwError(() => new Error('Error loading tasks'))); + + return component + .loadTasks(1) + .toPromise() + .catch(() => { + expect(component.tasks).toEqual([]); + }); + }); + }); - it('should handle errors when loading feedback details', () => { - // Mock loadFeedbackDetails to simulate an error - jest.spyOn(component, 'loadFeedbackDetails').mockReturnValue(throwError('Error')); + describe('loadFeedbackDetails', () => { + it('should load feedback details and update the component state', () => { + return component + .loadFeedbackDetails(1) + .toPromise() + .then(() => { + expect(component.resultIds).toEqual([1, 2]); + expect(component.feedback).toHaveLength(2); // feedbackMap will merge two entries with the same detailText and testCaseName + }); + }); + + it('should handle error while loading feedback details', () => { + getFeedbackDetailsSpy.mockReturnValue(throwError(() => new Error('Error loading feedback details'))); + + return component + .loadFeedbackDetails(1) + .toPromise() + .catch(() => { + expect(component.feedback).toEqual([]); + expect(component.resultIds).toEqual([]); + }); + }); + }); - component.loadFeedbackDetails(1); + describe('saveFeedback', () => { + it('should save feedbacks and sort them by count', () => { + component.saveFeedback(feedbackMock); - expect(component.loadFeedbackDetails).toHaveBeenCalled(); - expect(component.feedback).toHaveLength(0); + expect(component.feedback).toHaveLength(2); // Only 2 unique feedbacks + expect(component.feedback[0].count).toBe(2); // The first feedback should have count 2 + expect(component.feedback[1].count).toBe(1); // The second feedback should have count 1 + }); }); - it('should save feedbacks and sort them by count', () => { - component.saveFeedback(feedbackMock); + describe('findTaskIndexForTestCase', () => { + it('should find the correct task index for a given test case', () => { + component.tasks = tasksMock; - expect(component.feedback).toHaveLength(2); - expect(component.feedback[0].count).toBe(2); - expect(component.feedback[1].count).toBe(1); - expect(component.feedback[0].detailText).toBe('Test feedback 1 detail'); - }); + const index1 = component.findTaskIndexForTestCase('test1'); + expect(index1).toBe(1); - it('should find task index for a given test case', () => { - component.tasks = tasksMock; - const index = component.findTaskIndexForTestCase('test1'); - expect(index).toBe(1); + const index2 = component.findTaskIndexForTestCase('test2'); + expect(index2).toBe(2); - const zeroIndex = component.findTaskIndexForTestCase('test3'); - expect(zeroIndex).toBe(0); + const nonExistingIndex = component.findTaskIndexForTestCase('non-existing'); + expect(nonExistingIndex).toBe(0); // No task found, should return 0 + }); - const undefinedIndex = component.findTaskIndexForTestCase(''); - expect(undefinedIndex).toBe(-1); + it('should return -1 if testCaseName is not provided', () => { + const index = component.findTaskIndexForTestCase(''); + expect(index).toBe(-1); + }); }); }); From 4b22236363daf07ad640bc6f65b0af53e5374944 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 18 Aug 2024 17:10:13 +0200 Subject: [PATCH 24/52] coderabbit --- .../grading/testcase-analysis/testcase-analysis.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index 6d23eb27126e..f14b07bcb275 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -81,6 +81,6 @@ export class TestcaseAnalysisComponent implements OnInit { if (!testCaseName) { return -1; } - return this.tasks.findIndex((tasks) => tasks.testCases && tasks.testCases.some((tc) => tc.testName === testCaseName)) + 1; + return this.tasks.findIndex((tasks) => tasks.testCases?.some((tc) => tc.testName === testCaseName)) + 1; } } From cf9064b3ee029c78deb2bd70378902a4373ffebd Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 18 Aug 2024 17:15:27 +0200 Subject: [PATCH 25/52] tests --- .../testcase-analysis/testcase-analysis.component.ts | 8 ++++---- .../testcase-analysis/testcase-analysis.component.spec.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index f14b07bcb275..efe8ebf77279 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -35,7 +35,7 @@ export class TestcaseAnalysisComponent implements OnInit { } } - private loadTasks(exerciseId: number) { + loadTasks(exerciseId: number) { return this.simplifiedProgrammingExerciseTaskService.getSimplifiedTasks(exerciseId).pipe( tap((tasks) => { this.tasks = tasks; @@ -43,7 +43,7 @@ export class TestcaseAnalysisComponent implements OnInit { ); } - private loadFeedbackDetails(exerciseId: number) { + loadFeedbackDetails(exerciseId: number) { return this.simplifiedProgrammingExerciseTaskService.getFeedbackDetailsForExercise(exerciseId).pipe( tap((response) => { this.resultIds = response.body?.resultIds || []; @@ -53,7 +53,7 @@ export class TestcaseAnalysisComponent implements OnInit { ); } - private saveFeedback(feedbackArray: { detailText: string; testCaseName: string }[]): void { + saveFeedback(feedbackArray: { detailText: string; testCaseName: string }[]): void { const feedbackMap: Map = new Map(); feedbackArray.forEach((feedback) => { @@ -77,7 +77,7 @@ export class TestcaseAnalysisComponent implements OnInit { this.feedback = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); } - private findTaskIndexForTestCase(testCaseName: string): number { + findTaskIndexForTestCase(testCaseName: string): number { if (!testCaseName) { return -1; } diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 77457d941760..855f665f1250 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -57,7 +57,7 @@ describe('TestcaseAnalysisComponent', () => { it('should call loadTasks and loadFeedbackDetails when exerciseId is provided', () => { component.ngOnInit(); - return new Promise((resolve) => { + return new Promise((resolve) => { setTimeout(() => { expect(getSimplifiedTasksSpy).toHaveBeenCalledWith(1); expect(getFeedbackDetailsSpy).toHaveBeenCalledWith(1); From 4505ef6ad145216b9b9d55f649a212f5d348b18c Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 18 Aug 2024 17:59:51 +0200 Subject: [PATCH 26/52] tests --- .../testcase-analysis.component.ts | 3 +- .../testcase-analysis.service.spec.ts | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index efe8ebf77279..c6f5b6c7901f 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -1,8 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; -import { concatMap } from 'rxjs/operators'; -import { tap } from 'rxjs/operators'; +import { concatMap, tap } from 'rxjs/operators'; type FeedbackDetail = { count: number; diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts new file mode 100644 index 000000000000..ed6b4649e422 --- /dev/null +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts @@ -0,0 +1,92 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; +import { FeedbackDetailsWithResultIdsDTO } from 'app/exercises/shared/result/result.service'; +import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; + +describe('TestcaseAnalysisService', () => { + let service: TestcaseAnalysisService; + let httpMock: HttpTestingController; + + const feedbackDetailsMock: FeedbackDetailsWithResultIdsDTO = { + feedbackDetails: [ + { detailText: 'Feedback 1', testCaseName: 'test1' }, + { detailText: 'Feedback 2', testCaseName: 'test2' }, + ], + resultIds: [1, 2], + }; + + const simplifiedTasksMock: ProgrammingExerciseServerSideTask[] = [ + { taskName: 'Task 1', testCases: [{ testName: 'test1' }] as ProgrammingExerciseServerSideTask['testCases'] }, + { taskName: 'Task 2', testCases: [{ testName: 'test2' }] as ProgrammingExerciseServerSideTask['testCases'] }, + ]; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [TestcaseAnalysisService], + }); + + service = TestBed.inject(TestcaseAnalysisService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('getFeedbackDetailsForExercise', () => { + it('should retrieve feedback details for a given exercise', () => { + service.getFeedbackDetailsForExercise(1).subscribe((response) => { + expect(response.body).toEqual(feedbackDetailsMock); + }); + + const req = httpMock.expectOne('api/exercises/1/feedback-details'); + expect(req.request.method).toBe('GET'); + req.flush(feedbackDetailsMock); + }); + + it('should handle errors while retrieving feedback details', () => { + service.getFeedbackDetailsForExercise(1).subscribe({ + next: () => {}, + error: (error) => { + expect(error.status).toBe(500); + }, + }); + + const req = httpMock.expectOne('api/exercises/1/feedback-details'); + expect(req.request.method).toBe('GET'); + req.flush('Something went wrong', { status: 500, statusText: 'Server Error' }); + }); + }); + + describe('getSimplifiedTasks', () => { + it('should retrieve simplified tasks for a given exercise', () => { + const expectedTasks: SimplifiedTask[] = [ + { taskName: 'Task 1', testCases: [{ testName: 'test1' }] }, + { taskName: 'Task 2', testCases: [{ testName: 'test2' }] }, + ]; + + service.getSimplifiedTasks(1).subscribe((tasks) => { + expect(tasks).toEqual(expectedTasks); + }); + + const req = httpMock.expectOne('api/programming-exercises/1/tasks-with-unassigned-test-cases'); + expect(req.request.method).toBe('GET'); + req.flush(simplifiedTasksMock); + }); + + it('should handle errors while retrieving simplified tasks', () => { + service.getSimplifiedTasks(1).subscribe({ + next: () => {}, + error: (error) => { + expect(error.status).toBe(404); + }, + }); + + const req = httpMock.expectOne('api/programming-exercises/1/tasks-with-unassigned-test-cases'); + expect(req.request.method).toBe('GET'); + req.flush('Not Found', { status: 404, statusText: 'Not Found' }); + }); + }); +}); From aaec498ab4f5485871b4cd579fb0644ecde3d4d6 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 18 Aug 2024 18:16:41 +0200 Subject: [PATCH 27/52] moved interface --- .../grading/testcase-analysis/testcase-analysis.service.ts | 6 +++++- .../webapp/app/exercises/shared/result/result.service.ts | 5 ----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts index efd17daf892d..b6dc94e6026b 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts @@ -3,13 +3,17 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; -import { FeedbackDetailsWithResultIdsDTO } from 'app/exercises/shared/result/result.service'; export interface SimplifiedTask { taskName: string; testCases: ProgrammingExerciseServerSideTask['testCases']; } +export interface FeedbackDetailsWithResultIdsDTO { + feedbackDetails: { detailText: string; testCaseName: string }[]; + resultIds: number[]; +} + @Injectable() export class TestcaseAnalysisService { private resourceUrl = 'api/programming-exercises'; diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index 32321b49d799..72390f6979c8 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -46,11 +46,6 @@ export interface IResultService { triggerDownloadCSV: (rows: string[], csvFileName: string) => void; } -export interface FeedbackDetailsWithResultIdsDTO { - feedbackDetails: { detailText: string; testCaseName: string }[]; - resultIds: number[]; -} - @Injectable({ providedIn: 'root' }) export class ResultService implements IResultService { private exerciseResourceUrl = 'api/exercises'; From 3511e7963249571619dc1a7dedf542adc6a37a52 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 18 Aug 2024 18:25:39 +0200 Subject: [PATCH 28/52] fixed import --- .../testcase-analysis/testcase-analysis.service.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts index ed6b4649e422..523994e2c371 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts @@ -1,7 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; -import { FeedbackDetailsWithResultIdsDTO } from 'app/exercises/shared/result/result.service'; +import { FeedbackDetailsWithResultIdsDTO, SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; describe('TestcaseAnalysisService', () => { From 99602bc2e789a961faf0a2e538b9f3d232d6a678 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 18 Aug 2024 20:20:19 +0200 Subject: [PATCH 29/52] feedback implemented --- .../StudentParticipationRepository.java | 7 +- .../www1/artemis/web/rest/ResultResource.java | 5 +- .../rest/dto/feedback/FeedbackDetailDTO.java | 3 + .../FeedbackDetailsWithResultIdsDTO.java | 3 + .../testcase-analysis.component.ts | 6 +- .../webapp/i18n/de/programmingExercise.json | 10 +-- .../testcase-analysis.component.spec.ts | 78 ++++++++----------- 7 files changed, 52 insertions(+), 60 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index f2cadba7f43c..7e2eb08c078f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -1216,7 +1216,6 @@ SELECT COALESCE(AVG(p.presentationScore), 0) FROM StudentParticipation p LEFT JOIN FETCH p.results r LEFT JOIN FETCH r.submission s - LEFT JOIN FETCH r.assessmentNote LEFT JOIN FETCH r.feedbacks f LEFT JOIN FETCH f.testCase WHERE p.exercise.id = :exerciseId @@ -1226,7 +1225,7 @@ SELECT COALESCE(AVG(p.presentationScore), 0) OR r IS NULL ) """) - Set findByExerciseIdWithLatestResultAndFeedbackAndTestcases(@Param("exerciseId") long exerciseId); + Set findByExerciseIdWithLatestResultsAndFeedbackAndTestcases(@Param("exerciseId") long exerciseId); /** * Get the latest results and their feedbacks for a given exercise ID from the database (independant of if the correction was manual or automatic). @@ -1236,8 +1235,8 @@ SELECT COALESCE(AVG(p.presentationScore), 0) * @param exerciseId the ID of the exercise for which the latest results and feedbacks should be loaded from the database * @return the set of student participations containing the latest results and their feedbacks */ - default Set findByExerciseIdWithLatestResultAndFeedbackAndTestcasesElseThrow(long exerciseId) { - return getArbitraryValueElseThrow(Optional.of(findByExerciseIdWithLatestResultAndFeedbackAndTestcases(exerciseId)), "Results with Feedback for: " + exerciseId); + default Set findByExerciseIdWithLatestResultsAndFeedbackAndTestcasesElseThrow(long exerciseId) { + return getArbitraryValueElseThrow(Optional.of(findByExerciseIdWithLatestResultsAndFeedbackAndTestcases(exerciseId)), "Results with Feedback for: " + exerciseId); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 11e1c5ca7f00..557d6ef2162c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -46,6 +46,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; +import de.tum.in.www1.artemis.security.annotations.enforceRoleInExercise.EnforceAtLeastTutorInExercise; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.ParticipationAuthorizationCheckService; import de.tum.in.www1.artemis.service.ParticipationService; @@ -287,11 +288,11 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo * @return A ResponseEntity containing a list of all feedback details and the corresponding result IDs for the exercise. */ @GetMapping("exercises/{exerciseId}/feedback-details") - @EnforceAtLeastTutor + @EnforceAtLeastTutorInExercise public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); - Set participations = studentParticipationRepository.findByExerciseIdWithLatestResultAndFeedbackAndTestcasesElseThrow(exerciseId); + Set participations = studentParticipationRepository.findByExerciseIdWithLatestResultsAndFeedbackAndTestcasesElseThrow(exerciseId); removeSubmissionAndExerciseData(participations); List allFeedbackDetails = new ArrayList<>(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java index 86b8ccc5ea69..53f52c85c5ad 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java @@ -1,4 +1,7 @@ package de.tum.in.www1.artemis.web.rest.dto.feedback; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) public record FeedbackDetailDTO(String detailText, String testCaseName) { } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java index 63748a8dc6e6..49c21870dfa3 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java @@ -2,5 +2,8 @@ import java.util.List; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) public record FeedbackDetailsWithResultIdsDTO(List feedbackDetails, List resultIds) { } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index c6f5b6c7901f..ceae57998301 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -64,7 +64,7 @@ export class TestcaseAnalysisComponent implements OnInit { if (existingFeedback) { existingFeedback.count += 1; } else { - const task = this.findTaskIndexForTestCase(testcase); + const task = this.taskIndex(testcase); feedbackMap.set(key, { count: 1, detailText: feedbackText, @@ -76,9 +76,9 @@ export class TestcaseAnalysisComponent implements OnInit { this.feedback = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); } - findTaskIndexForTestCase(testCaseName: string): number { + taskIndex(testCaseName: string): number { if (!testCaseName) { - return -1; + return 0; } return this.tasks.findIndex((tasks) => tasks.testCases?.some((tc) => tc.testName === testCaseName)) + 1; } diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 97518c438fb4..ae7454ab29f9 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -309,13 +309,13 @@ }, "testAnalysis": { "titleHeader": "Test Analyse", - "title": "Fehler Analyse für {{exerciseTitle}}", + "title": "Fehleranalyse für {{exerciseTitle}}", "occurrence": "Häufigkeit", - "testCaseFeedback": "Test Fall Feedback", + "testCaseFeedback": "Testfall Feedback", "task": "Aufgabe", - "testcase": "Test Fall", - "errorCategory": "Fehler Kategorie", - "totalItems": "Insgesamt {{count}} stück" + "testcase": "Testfalll", + "errorCategory": "Fehlerkategorie", + "totalItems": "Insgesamt {{count}} Elemente" }, "help": { "name": "Aufgabennamen werden fett geschrieben, während Testnamen normal sind. Ob es ein Aufgabenname oder Testname ist hängt davon ab, ob die Reihe eine Aufgabe oder einen Test darstellt.", diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 855f665f1250..80e8bd56636d 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../test.module'; @@ -56,16 +56,12 @@ describe('TestcaseAnalysisComponent', () => { describe('ngOnInit', () => { it('should call loadTasks and loadFeedbackDetails when exerciseId is provided', () => { component.ngOnInit(); + fixture.whenStable(); - return new Promise((resolve) => { - setTimeout(() => { - expect(getSimplifiedTasksSpy).toHaveBeenCalledWith(1); - expect(getFeedbackDetailsSpy).toHaveBeenCalledWith(1); - expect(component.tasks).toEqual(tasksMock); - expect(component.resultIds).toEqual([1, 2]); - resolve(); - }, 0); - }); + expect(getSimplifiedTasksSpy).toHaveBeenCalledWith(1); + expect(getFeedbackDetailsSpy).toHaveBeenCalledWith(1); + expect(component.tasks).toEqual(tasksMock); + expect(component.resultIds).toEqual([1, 2]); }); it('should not call loadTasks and loadFeedbackDetails if exerciseId is not provided', () => { @@ -79,47 +75,37 @@ describe('TestcaseAnalysisComponent', () => { describe('loadTasks', () => { it('should load tasks and update the component state', () => { - return component - .loadTasks(1) - .toPromise() - .then(() => { - expect(component.tasks).toEqual(tasksMock); - }); + firstValueFrom(component.loadTasks(1)); + expect(component.tasks).toEqual(tasksMock); }); it('should handle error while loading tasks', () => { getSimplifiedTasksSpy.mockReturnValue(throwError(() => new Error('Error loading tasks'))); - return component - .loadTasks(1) - .toPromise() - .catch(() => { - expect(component.tasks).toEqual([]); - }); + try { + firstValueFrom(component.loadTasks(1)); + } catch { + expect(component.tasks).toEqual([]); + } }); }); describe('loadFeedbackDetails', () => { it('should load feedback details and update the component state', () => { - return component - .loadFeedbackDetails(1) - .toPromise() - .then(() => { - expect(component.resultIds).toEqual([1, 2]); - expect(component.feedback).toHaveLength(2); // feedbackMap will merge two entries with the same detailText and testCaseName - }); + firstValueFrom(component.loadFeedbackDetails(1)); + expect(component.resultIds).toEqual([1, 2]); + expect(component.feedback).toHaveLength(2); }); it('should handle error while loading feedback details', () => { getFeedbackDetailsSpy.mockReturnValue(throwError(() => new Error('Error loading feedback details'))); - return component - .loadFeedbackDetails(1) - .toPromise() - .catch(() => { - expect(component.feedback).toEqual([]); - expect(component.resultIds).toEqual([]); - }); + try { + firstValueFrom(component.loadFeedbackDetails(1)); + } catch { + expect(component.feedback).toEqual([]); + expect(component.resultIds).toEqual([]); + } }); }); @@ -127,29 +113,29 @@ describe('TestcaseAnalysisComponent', () => { it('should save feedbacks and sort them by count', () => { component.saveFeedback(feedbackMock); - expect(component.feedback).toHaveLength(2); // Only 2 unique feedbacks - expect(component.feedback[0].count).toBe(2); // The first feedback should have count 2 - expect(component.feedback[1].count).toBe(1); // The second feedback should have count 1 + expect(component.feedback).toHaveLength(2); + expect(component.feedback[0].count).toBe(2); + expect(component.feedback[1].count).toBe(1); }); }); - describe('findTaskIndexForTestCase', () => { + describe('taskIndex', () => { it('should find the correct task index for a given test case', () => { component.tasks = tasksMock; - const index1 = component.findTaskIndexForTestCase('test1'); + const index1 = component.taskIndex('test1'); expect(index1).toBe(1); - const index2 = component.findTaskIndexForTestCase('test2'); + const index2 = component.taskIndex('test2'); expect(index2).toBe(2); - const nonExistingIndex = component.findTaskIndexForTestCase('non-existing'); - expect(nonExistingIndex).toBe(0); // No task found, should return 0 + const nonExistingIndex = component.taskIndex('non-existing'); + expect(nonExistingIndex).toBe(0); }); it('should return -1 if testCaseName is not provided', () => { - const index = component.findTaskIndexForTestCase(''); - expect(index).toBe(-1); + const index = component.taskIndex(''); + expect(index).toBe(0); }); }); }); From cced3ab5a9be698b217e0778c30fad2bfc144adc Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 18 Aug 2024 20:39:40 +0200 Subject: [PATCH 30/52] feedback implemented --- .../testcase-analysis/testcase-analysis.component.ts | 8 +++++--- src/main/webapp/i18n/de/programmingExercise.json | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index ceae57998301..e446ab823507 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -1,7 +1,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; +import { FeedbackDetailsWithResultIdsDTO, SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; +import { Observable } from 'rxjs'; import { concatMap, tap } from 'rxjs/operators'; +import { HttpResponse } from '@angular/common/http'; type FeedbackDetail = { count: number; @@ -34,7 +36,7 @@ export class TestcaseAnalysisComponent implements OnInit { } } - loadTasks(exerciseId: number) { + loadTasks(exerciseId: number): Observable { return this.simplifiedProgrammingExerciseTaskService.getSimplifiedTasks(exerciseId).pipe( tap((tasks) => { this.tasks = tasks; @@ -42,7 +44,7 @@ export class TestcaseAnalysisComponent implements OnInit { ); } - loadFeedbackDetails(exerciseId: number) { + loadFeedbackDetails(exerciseId: number): Observable> { return this.simplifiedProgrammingExerciseTaskService.getFeedbackDetailsForExercise(exerciseId).pipe( tap((response) => { this.resultIds = response.body?.resultIds || []; diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index ae7454ab29f9..b5e526781165 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -313,7 +313,7 @@ "occurrence": "Häufigkeit", "testCaseFeedback": "Testfall Feedback", "task": "Aufgabe", - "testcase": "Testfalll", + "testcase": "Testfall", "errorCategory": "Fehlerkategorie", "totalItems": "Insgesamt {{count}} Elemente" }, From 50795d771d5cdb10be5150d080f368ca16960c67 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 19 Aug 2024 00:30:53 +0200 Subject: [PATCH 31/52] feedback implemented --- .../StudentParticipationRepository.java | 28 ------------------ .../www1/artemis/web/rest/ResultResource.java | 7 +++-- .../ResultServiceIntegrationTest.java | 29 +++++-------------- 3 files changed, 12 insertions(+), 52 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 7e2eb08c078f..7d961f6a0dd3 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -1211,32 +1211,4 @@ SELECT COALESCE(AVG(p.presentationScore), 0) """) double getAvgPresentationScoreByCourseId(@Param("courseId") long courseId); - @Query(""" - SELECT DISTINCT p - FROM StudentParticipation p - LEFT JOIN FETCH p.results r - LEFT JOIN FETCH r.submission s - LEFT JOIN FETCH r.feedbacks f - LEFT JOIN FETCH f.testCase - WHERE p.exercise.id = :exerciseId - AND ( - r.id = (SELECT MAX(p_r.id) FROM p.results p_r) - OR r.assessmentType <> de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC - OR r IS NULL - ) - """) - Set findByExerciseIdWithLatestResultsAndFeedbackAndTestcases(@Param("exerciseId") long exerciseId); - - /** - * Get the latest results and their feedbacks for a given exercise ID from the database (independant of if the correction was manual or automatic). - * The results are loaded together with their feedback and testcases. - * Throws an EntityNotFoundException if no results could be found for the given exercise ID. - * - * @param exerciseId the ID of the exercise for which the latest results and feedbacks should be loaded from the database - * @return the set of student participations containing the latest results and their feedbacks - */ - default Set findByExerciseIdWithLatestResultsAndFeedbackAndTestcasesElseThrow(long exerciseId) { - return getArbitraryValueElseThrow(Optional.of(findByExerciseIdWithLatestResultsAndFeedbackAndTestcases(exerciseId)), "Results with Feedback for: " + exerciseId); - } - } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 557d6ef2162c..368e7f37ab21 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -292,7 +292,8 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); - Set participations = studentParticipationRepository.findByExerciseIdWithLatestResultsAndFeedbackAndTestcasesElseThrow(exerciseId); + List participations = studentParticipationRepository + .findByExerciseIdWithLatestAutomaticResultAndFeedbacksAndTestCasesWithoutIndividualDueDate(exerciseId); removeSubmissionAndExerciseData(participations); List allFeedbackDetails = new ArrayList<>(); @@ -313,10 +314,10 @@ public ResponseEntity getAllFeedbackDetailsForE }); FeedbackDetailsWithResultIdsDTO response = new FeedbackDetailsWithResultIdsDTO(allFeedbackDetails, resultIds); - return new ResponseEntity<>(response, HttpStatus.OK); + return ResponseEntity.ok(response); } - private void removeSubmissionAndExerciseData(Set participations) { + private void removeSubmissionAndExerciseData(List participations) { // remove unnecessary data to reduce response size participations.forEach(participation -> { participation.setSubmissions(null); diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index d45f1a5232cc..db3c06cbdc91 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -726,11 +726,12 @@ void testGetAssessmentCountByCorrectionRoundForProgrammingExercise() { } @Test - @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetAllFeedbackDetailsForExercise() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); - Result result = participationUtilService.addResultToParticipation(null, null, participation); + + Result result = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation); Feedback feedback = new Feedback(); feedback.setPositive(false); @@ -744,35 +745,21 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { assertThat(response.feedbackDetails()).isNotEmpty(); assertThat(response.resultIds()).containsExactly(result.getId()); - FeedbackDetailDTO feedbackDetail = response.feedbackDetails().getFirst(); + FeedbackDetailDTO feedbackDetail = response.feedbackDetails().get(0); assertThat(feedbackDetail.detailText()).isEqualTo("Some feedback"); - assertThat(feedbackDetail.testCaseName()).isNull(); + assertThat(feedbackDetail.testCaseName()).isNull(); // Test case name should be null since no test case was linked } @Test - @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") - void testGetAllFeedbackDetailsForExercise_NoFeedback() throws Exception { - ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); - StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); - Result result = participationUtilService.addResultToParticipation(null, null, participation); - - FeedbackDetailsWithResultIdsDTO response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, - FeedbackDetailsWithResultIdsDTO.class); - - assertThat(response.feedbackDetails()).isEmpty(); - assertThat(response.resultIds()).containsExactly(result.getId()); - } - - @Test - @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetAllFeedbackDetailsForExercise_NoParticipations() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); FeedbackDetailsWithResultIdsDTO response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailsWithResultIdsDTO.class); - assertThat(response.feedbackDetails()).isEmpty(); - assertThat(response.resultIds()).isEmpty(); + assertThat(response.feedbackDetails()).isNull(); + assertThat(response.resultIds()).isNull(); } } From faf33ccb8904af3ce56e1594fea462a401c00327 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 19 Aug 2024 18:45:43 +0200 Subject: [PATCH 32/52] feedback Markus/Ramona implemented --- .../StudentParticipationRepository.java | 1 - .../www1/artemis/web/rest/ResultResource.java | 22 +++------ ...-exercise-configure-grading.component.html | 14 ++++-- .../testcase-analysis.component.html | 6 +-- .../testcase-analysis.component.ts | 21 +++++++-- .../testcase-analysis.service.ts | 45 ++++++++++++++----- 6 files changed, 70 insertions(+), 39 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 7d961f6a0dd3..8cd15a15b573 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -1210,5 +1210,4 @@ SELECT COALESCE(AVG(p.presentationScore), 0) AND p.presentationScore IS NOT NULL """) double getAvgPresentationScoreByCourseId(@Param("courseId") long courseId); - } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 368e7f37ab21..8a9a7ac99a22 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -46,7 +46,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; -import de.tum.in.www1.artemis.security.annotations.enforceRoleInExercise.EnforceAtLeastTutorInExercise; +import de.tum.in.www1.artemis.security.annotations.enforceRoleInExercise.EnforceAtLeastEditorInExercise; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.ParticipationAuthorizationCheckService; import de.tum.in.www1.artemis.service.ParticipationService; @@ -288,7 +288,7 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo * @return A ResponseEntity containing a list of all feedback details and the corresponding result IDs for the exercise. */ @GetMapping("exercises/{exerciseId}/feedback-details") - @EnforceAtLeastTutorInExercise + @EnforceAtLeastEditorInExercise public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); @@ -296,22 +296,12 @@ public ResponseEntity getAllFeedbackDetailsForE .findByExerciseIdWithLatestAutomaticResultAndFeedbacksAndTestCasesWithoutIndividualDueDate(exerciseId); removeSubmissionAndExerciseData(participations); - List allFeedbackDetails = new ArrayList<>(); List resultIds = new ArrayList<>(); - participations.forEach(participation -> { - participation.getResults().forEach(result -> { - resultIds.add(result.getId()); - result.getFeedbacks().forEach(feedback -> { - if (Boolean.FALSE.equals(feedback.isPositive())) { - String detailText = feedback.getDetailText(); - String testCaseName = feedback.getTestCase() != null ? feedback.getTestCase().getTestName() : null; - FeedbackDetailDTO feedbackDetailDTO = new FeedbackDetailDTO(detailText, testCaseName); - allFeedbackDetails.add(feedbackDetailDTO); - } - }); - }); - }); + List allFeedbackDetails = new ArrayList<>(participations.stream().filter(participation -> !participation.isPracticeMode()) + .flatMap(participation -> participation.getResults().stream()).peek(result -> resultIds.add(result.getId())).flatMap(result -> result.getFeedbacks().stream()) + .filter(feedback -> Boolean.FALSE.equals(feedback.isPositive())) + .map(feedback -> new FeedbackDetailDTO(feedback.getDetailText(), (feedback.getTestCase() != null ? feedback.getTestCase().getTestName() : null))).toList()); FeedbackDetailsWithResultIdsDTO response = new FeedbackDetailsWithResultIdsDTO(allFeedbackDetails, resultIds); return ResponseEntity.ok(response); diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index 858e6643fc26..d4777262c8eb 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -16,9 +16,11 @@

-
- -
+ @if (programmingExercise.isAtLeastEditor) { +
+ +
+ }
@@ -261,7 +263,11 @@

@if (activeTab === 'testcase-analysis') { - + }
} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html index b1a237421357..252c15307822 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html @@ -1,6 +1,6 @@ -
+

-

+
@@ -13,7 +13,7 @@

@for (item of feedback; track item) {

- + diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts index e446ab823507..4cf6a57d941a 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts @@ -7,6 +7,7 @@ import { HttpResponse } from '@angular/common/http'; type FeedbackDetail = { count: number; + relativeCount: number; detailText: string; testcase: string; task: number; @@ -22,6 +23,7 @@ type FeedbackDetail = { export class TestcaseAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; @Input() exerciseId?: number; + @Input() isAtLeastEditor!: undefined | boolean; resultIds: number[] = []; tasks: SimplifiedTask[] = []; feedback: FeedbackDetail[] = []; @@ -29,10 +31,15 @@ export class TestcaseAnalysisComponent implements OnInit { constructor(private simplifiedProgrammingExerciseTaskService: TestcaseAnalysisService) {} ngOnInit(): void { - if (this.exerciseId) { - this.loadTasks(this.exerciseId) - .pipe(concatMap(() => this.loadFeedbackDetails(this.exerciseId!))) - .subscribe(); + if (this.isAtLeastEditor) { + this.simplifiedProgrammingExerciseTaskService.isAtLeastEditor = this.isAtLeastEditor; + if (this.exerciseId) { + this.loadTasks(this.exerciseId) + .pipe(concatMap(() => this.loadFeedbackDetails(this.exerciseId!))) + .subscribe(); + } + } else { + this.simplifiedProgrammingExerciseTaskService.isAtLeastEditor = false; } } @@ -65,10 +72,12 @@ export class TestcaseAnalysisComponent implements OnInit { const existingFeedback = feedbackMap.get(key); if (existingFeedback) { existingFeedback.count += 1; + existingFeedback.relativeCount = this.getRelativeCount(existingFeedback.count); } else { const task = this.taskIndex(testcase); feedbackMap.set(key, { count: 1, + relativeCount: this.getRelativeCount(1), detailText: feedbackText, testcase: testcase, task: task, @@ -84,4 +93,8 @@ export class TestcaseAnalysisComponent implements OnInit { } return this.tasks.findIndex((tasks) => tasks.testCases?.some((tc) => tc.testName === testCaseName)) + 1; } + + getRelativeCount(count: number): number { + return (count / this.resultIds.length) * 100; + } } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts index b6dc94e6026b..7e1dbcc42b09 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpResponse } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { HttpClient, HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; import { map } from 'rxjs/operators'; import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; @@ -18,21 +18,44 @@ export interface FeedbackDetailsWithResultIdsDTO { export class TestcaseAnalysisService { private resourceUrl = 'api/programming-exercises'; private exerciseResourceUrl = 'api/exercises'; + isAtLeastEditor = false; constructor(private http: HttpClient) {} getFeedbackDetailsForExercise(exerciseId: number): Observable> { - return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); + if (this.isAtLeastEditor) { + return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); + } else { + return throwError( + () => + new HttpErrorResponse({ + status: 403, + statusText: 'Forbidden', + error: 'User does not have permission to access this resource.', + }), + ); + } } public getSimplifiedTasks(exerciseId: number): Observable { - return this.http.get(`${this.resourceUrl}/${exerciseId}/tasks-with-unassigned-test-cases`).pipe( - map((tasks) => - tasks.map((task) => ({ - taskName: task.taskName ?? '', - testCases: task.testCases ?? [], - })), - ), - ); + if (this.isAtLeastEditor) { + return this.http.get(`${this.resourceUrl}/${exerciseId}/tasks-with-unassigned-test-cases`).pipe( + map((tasks) => + tasks.map((task) => ({ + taskName: task.taskName ?? '', + testCases: task.testCases ?? [], + })), + ), + ); + } else { + return throwError( + () => + new HttpErrorResponse({ + status: 403, + statusText: 'Forbidden', + error: 'User does not have permission to access this resource.', + }), + ); + } } } From d5fa0f70b21d37165cc86166bdf610f8d72bffd6 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 15:20:30 +0200 Subject: [PATCH 33/52] feedback Ramona/Markus implemented --- ...ng-exercise-configure-grading.component.ts | 3 +- .../programming-exercise-grading.module.ts | 4 +- ....html => feedback-analysis.component.html} | 6 +- ...nent.ts => feedback-analysis.component.ts} | 45 ++++++------ .../feedback-analysis.service.ts | 39 +++++++++++ .../testcase-analysis.service.ts | 61 ---------------- .../webapp/i18n/en/programmingExercise.json | 10 +-- .../testcase-analysis.component.spec.ts | 70 +++++++++++-------- .../testcase-analysis.service.spec.ts | 14 ++-- 9 files changed, 122 insertions(+), 130 deletions(-) rename src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/{testcase-analysis.component.html => feedback-analysis.component.html} (91%) rename src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/{testcase-analysis.component.ts => feedback-analysis.component.ts} (65%) create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts delete mode 100644 src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts index c6e93e45d271..0d8e0cb1e260 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts @@ -231,7 +231,8 @@ export class ProgrammingExerciseConfigureGradingComponent implements OnInit, OnD this.isLoading = false; } - if (params['tab'] === 'test-cases' || params['tab'] === 'code-analysis' || params['tab'] === 'submission-policy' || params['tab'] === 'testcase-analysis') { + const gradingTabs: GradingTab[] = ['test-cases', 'code-analysis', 'submission-policy', 'testcase-analysis']; + if (gradingTabs.includes(params['tab'])) { this.selectTab(params['tab']); } else { this.selectTab('test-cases'); diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts index 596127ddd9dc..1b84f3bb65b6 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts @@ -19,7 +19,7 @@ import { SubmissionPolicyUpdateModule } from 'app/exercises/shared/submission-po import { ProgrammingExerciseGradingTasksTableComponent } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-grading-tasks-table.component'; import { BarChartModule } from '@swimlane/ngx-charts'; import { ProgrammingExerciseTaskComponent } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task/programming-exercise-task.component'; -import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component'; +import { FeedbackAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component'; @NgModule({ imports: [ @@ -34,7 +34,7 @@ import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grad ArtemisProgrammingExerciseActionsModule, SubmissionPolicyUpdateModule, BarChartModule, - TestcaseAnalysisComponent, + FeedbackAnalysisComponent, ], declarations: [ ProgrammingExerciseConfigureGradingComponent, diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.html similarity index 91% rename from src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html rename to src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.html index 252c15307822..cc7abf0fe391 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.html @@ -11,17 +11,17 @@

- @for (item of feedback; track item) { + @for (item of feedbackDetails; track item) {

- + }
{{ item.count }} ({{ (this.resultIds.length > 0 ? item.count / this.resultIds.length : 0) * 100 | number: '1.0-0' }}%){{ item.count }} ({{ item.relativeCount | number: '1.0-0' }}%) {{ item.detailText }} {{ item.task }} {{ item.testcase }}
{{ item.count }} ({{ item.relativeCount | number: '1.0-0' }}%) {{ item.detailText }} {{ item.task }}{{ item.testcase }}{{ item.testCaseName }} Student Error
-
+
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts similarity index 65% rename from src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts rename to src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts index 4cf6a57d941a..e7b0f9f2449b 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts @@ -1,45 +1,48 @@ import { Component, Input, OnInit } from '@angular/core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { FeedbackDetailsWithResultIdsDTO, SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; +import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service'; import { Observable } from 'rxjs'; import { concatMap, tap } from 'rxjs/operators'; import { HttpResponse } from '@angular/common/http'; +import { AlertService } from 'app/core/util/alert.service'; -type FeedbackDetail = { +export interface FeedbackDetail { count: number; relativeCount: number; detailText: string; - testcase: string; + testCaseName: string; task: number; -}; +} @Component({ selector: 'jhi-testcase-analysis', - templateUrl: './testcase-analysis.component.html', + templateUrl: './feedback-analysis.component.html', standalone: true, imports: [ArtemisSharedModule], - providers: [TestcaseAnalysisService], + providers: [FeedbackAnalysisService], }) -export class TestcaseAnalysisComponent implements OnInit { +export class FeedbackAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; @Input() exerciseId?: number; @Input() isAtLeastEditor!: undefined | boolean; resultIds: number[] = []; tasks: SimplifiedTask[] = []; - feedback: FeedbackDetail[] = []; + feedbackDetails: FeedbackDetail[] = []; - constructor(private simplifiedProgrammingExerciseTaskService: TestcaseAnalysisService) {} + constructor( + private simplifiedProgrammingExerciseTaskService: FeedbackAnalysisService, + private alertService: AlertService, + ) {} ngOnInit(): void { if (this.isAtLeastEditor) { - this.simplifiedProgrammingExerciseTaskService.isAtLeastEditor = this.isAtLeastEditor; if (this.exerciseId) { this.loadTasks(this.exerciseId) .pipe(concatMap(() => this.loadFeedbackDetails(this.exerciseId!))) .subscribe(); } } else { - this.simplifiedProgrammingExerciseTaskService.isAtLeastEditor = false; + this.alertService.error('Permission Denied'); } } @@ -55,43 +58,43 @@ export class TestcaseAnalysisComponent implements OnInit { return this.simplifiedProgrammingExerciseTaskService.getFeedbackDetailsForExercise(exerciseId).pipe( tap((response) => { this.resultIds = response.body?.resultIds || []; - const feedbackArray = response.body?.feedbackDetails || []; - this.saveFeedback(feedbackArray); + const feedbackDetails = response.body?.feedbackDetails || []; + this.saveFeedback(feedbackDetails); }), ); } - saveFeedback(feedbackArray: { detailText: string; testCaseName: string }[]): void { + saveFeedback(feedbackDetails: FeedbackDetail[]): void { const feedbackMap: Map = new Map(); - feedbackArray.forEach((feedback) => { + feedbackDetails.forEach((feedback) => { const feedbackText = feedback.detailText ?? ''; - const testcase = feedback.testCaseName ?? ''; - const key = `${feedbackText}_${testcase}`; + const testCaseName = feedback.testCaseName ?? ''; + const key = `${feedbackText}_${testCaseName}`; const existingFeedback = feedbackMap.get(key); if (existingFeedback) { existingFeedback.count += 1; existingFeedback.relativeCount = this.getRelativeCount(existingFeedback.count); } else { - const task = this.taskIndex(testcase); + const task = this.taskIndex(testCaseName); feedbackMap.set(key, { count: 1, relativeCount: this.getRelativeCount(1), detailText: feedbackText, - testcase: testcase, + testCaseName: testCaseName, task: task, }); } }); - this.feedback = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); + this.feedbackDetails = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); } taskIndex(testCaseName: string): number { if (!testCaseName) { return 0; } - return this.tasks.findIndex((tasks) => tasks.testCases?.some((tc) => tc.testName === testCaseName)) + 1; + return this.tasks.findIndex((tasks) => tasks.testCases?.some((testCase) => testCase.testName === testCaseName)) + 1; } getRelativeCount(count: number): number { diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts new file mode 100644 index 000000000000..6a894fcf4403 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; +import { FeedbackDetail } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component'; + +export interface SimplifiedTask { + taskName: string; + testCases: ProgrammingExerciseServerSideTask['testCases']; +} + +export interface FeedbackDetailsWithResultIdsDTO { + feedbackDetails: FeedbackDetail[]; + resultIds: number[]; +} + +@Injectable() +export class FeedbackAnalysisService { + private resourceUrl = 'api/programming-exercises'; + private exerciseResourceUrl = 'api/exercises'; + + constructor(private http: HttpClient) {} + + getFeedbackDetailsForExercise(exerciseId: number): Observable> { + return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); + } + + public getSimplifiedTasks(exerciseId: number): Observable { + return this.http.get(`${this.resourceUrl}/${exerciseId}/tasks-with-unassigned-test-cases`).pipe( + map((tasks) => + tasks.map((task) => ({ + taskName: task.taskName ?? '', + testCases: task.testCases ?? [], + })), + ), + ); + } +} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts deleted file mode 100644 index 7e1dbcc42b09..000000000000 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient, HttpErrorResponse, HttpResponse } from '@angular/common/http'; -import { Observable, throwError } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; - -export interface SimplifiedTask { - taskName: string; - testCases: ProgrammingExerciseServerSideTask['testCases']; -} - -export interface FeedbackDetailsWithResultIdsDTO { - feedbackDetails: { detailText: string; testCaseName: string }[]; - resultIds: number[]; -} - -@Injectable() -export class TestcaseAnalysisService { - private resourceUrl = 'api/programming-exercises'; - private exerciseResourceUrl = 'api/exercises'; - isAtLeastEditor = false; - - constructor(private http: HttpClient) {} - - getFeedbackDetailsForExercise(exerciseId: number): Observable> { - if (this.isAtLeastEditor) { - return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); - } else { - return throwError( - () => - new HttpErrorResponse({ - status: 403, - statusText: 'Forbidden', - error: 'User does not have permission to access this resource.', - }), - ); - } - } - - public getSimplifiedTasks(exerciseId: number): Observable { - if (this.isAtLeastEditor) { - return this.http.get(`${this.resourceUrl}/${exerciseId}/tasks-with-unassigned-test-cases`).pipe( - map((tasks) => - tasks.map((task) => ({ - taskName: task.taskName ?? '', - testCases: task.testCases ?? [], - })), - ), - ); - } else { - return throwError( - () => - new HttpErrorResponse({ - status: 403, - statusText: 'Forbidden', - error: 'User does not have permission to access this resource.', - }), - ); - } - } -} diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index edf01f60479d..647214a7c3fb 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -309,13 +309,13 @@ "testType": "Type", "passedPercent": "Passed %" }, - "testAnalysis": { - "titleHeader": "Test Analysis", - "title": "Error Analysis for {{exerciseTitle}}", + "feedbackAnalysis": { + "titleHeader": "Feedback Analysis", + "title": "Feedback Analysis for {{exerciseTitle}}", "occurrence": "Occurrence", - "testCaseFeedback": "Test Case Feedback", + "feedback": "Feedback", "task": "Task", - "testcase": "Testcase", + "testcase": "Test Case", "errorCategory": "Error Category", "totalItems": "In total {{count}} items" }, diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 80e8bd56636d..776217fdcdfc 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -3,21 +3,22 @@ import { firstValueFrom, of, throwError } from 'rxjs'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../test.module'; -import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component'; -import { TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; +import { FeedbackAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component'; +import { FeedbackAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service'; import { HttpResponse } from '@angular/common/http'; +import { FeedbackDetail } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component'; -describe('TestcaseAnalysisComponent', () => { - let fixture: ComponentFixture; - let component: TestcaseAnalysisComponent; - let testcaseAnalysisService: TestcaseAnalysisService; +describe('FeedbackAnalysisComponent', () => { + let fixture: ComponentFixture; + let component: FeedbackAnalysisComponent; + let testcaseAnalysisService: FeedbackAnalysisService; let getSimplifiedTasksSpy: jest.SpyInstance; let getFeedbackDetailsSpy: jest.SpyInstance; - const feedbackMock = [ - { detailText: 'Test feedback 1 detail', testCaseName: 'test1' }, - { detailText: 'Test feedback 2 detail', testCaseName: 'test2' }, - { detailText: 'Test feedback 1 detail', testCaseName: 'test1' }, + const feedbackMock: FeedbackDetail[] = [ + { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 0, relativeCount: 0, task: 0 }, + { detailText: 'Test feedback 2 detail', testCaseName: 'test2', count: 0, relativeCount: 0, task: 0 }, + { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 0, relativeCount: 0, task: 0 }, ]; const tasksMock = [ @@ -31,22 +32,22 @@ describe('TestcaseAnalysisComponent', () => { beforeEach(() => { return TestBed.configureTestingModule({ - imports: [ArtemisTestModule, TranslateModule.forRoot(), TestcaseAnalysisComponent], + imports: [ArtemisTestModule, TranslateModule.forRoot(), FeedbackAnalysisComponent], providers: [ { provide: TranslateService, useClass: MockTranslateService, }, - TestcaseAnalysisService, + FeedbackAnalysisService, ], }) .compileComponents() .then(() => { - fixture = TestBed.createComponent(TestcaseAnalysisComponent); + fixture = TestBed.createComponent(FeedbackAnalysisComponent); component = fixture.componentInstance; component.exerciseId = 1; - testcaseAnalysisService = fixture.debugElement.injector.get(TestcaseAnalysisService); + testcaseAnalysisService = fixture.debugElement.injector.get(FeedbackAnalysisService); getSimplifiedTasksSpy = jest.spyOn(testcaseAnalysisService, 'getSimplifiedTasks').mockReturnValue(of(tasksMock)); getFeedbackDetailsSpy = jest.spyOn(testcaseAnalysisService, 'getFeedbackDetailsForExercise').mockReturnValue(of(feedbackDetailsResponseMock)); @@ -54,9 +55,10 @@ describe('TestcaseAnalysisComponent', () => { }); describe('ngOnInit', () => { - it('should call loadTasks and loadFeedbackDetails when exerciseId is provided', () => { + it('should call loadTasks and loadFeedbackDetails when exerciseId is provided', async () => { + component.isAtLeastEditor = true; component.ngOnInit(); - fixture.whenStable(); + await fixture.whenStable(); expect(getSimplifiedTasksSpy).toHaveBeenCalledWith(1); expect(getFeedbackDetailsSpy).toHaveBeenCalledWith(1); @@ -71,19 +73,27 @@ describe('TestcaseAnalysisComponent', () => { expect(getSimplifiedTasksSpy).not.toHaveBeenCalled(); expect(getFeedbackDetailsSpy).not.toHaveBeenCalled(); }); + + it('should not call loadTasks and loadFeedbackDetails if user is not at least editor', () => { + component.isAtLeastEditor = false; + component.ngOnInit(); + + expect(getSimplifiedTasksSpy).not.toHaveBeenCalled(); + expect(getFeedbackDetailsSpy).not.toHaveBeenCalled(); + }); }); describe('loadTasks', () => { - it('should load tasks and update the component state', () => { - firstValueFrom(component.loadTasks(1)); + it('should load tasks and update the component state', async () => { + await firstValueFrom(component.loadTasks(1)); expect(component.tasks).toEqual(tasksMock); }); - it('should handle error while loading tasks', () => { + it('should handle error while loading tasks', async () => { getSimplifiedTasksSpy.mockReturnValue(throwError(() => new Error('Error loading tasks'))); try { - firstValueFrom(component.loadTasks(1)); + await firstValueFrom(component.loadTasks(1)); } catch { expect(component.tasks).toEqual([]); } @@ -91,19 +101,19 @@ describe('TestcaseAnalysisComponent', () => { }); describe('loadFeedbackDetails', () => { - it('should load feedback details and update the component state', () => { - firstValueFrom(component.loadFeedbackDetails(1)); + it('should load feedback details and update the component state', async () => { + await firstValueFrom(component.loadFeedbackDetails(1)); expect(component.resultIds).toEqual([1, 2]); - expect(component.feedback).toHaveLength(2); + expect(component.feedbackDetails).toHaveLength(2); }); - it('should handle error while loading feedback details', () => { + it('should handle error while loading feedback details', async () => { getFeedbackDetailsSpy.mockReturnValue(throwError(() => new Error('Error loading feedback details'))); try { - firstValueFrom(component.loadFeedbackDetails(1)); + await firstValueFrom(component.loadFeedbackDetails(1)); } catch { - expect(component.feedback).toEqual([]); + expect(component.feedbackDetails).toEqual([]); expect(component.resultIds).toEqual([]); } }); @@ -113,9 +123,9 @@ describe('TestcaseAnalysisComponent', () => { it('should save feedbacks and sort them by count', () => { component.saveFeedback(feedbackMock); - expect(component.feedback).toHaveLength(2); - expect(component.feedback[0].count).toBe(2); - expect(component.feedback[1].count).toBe(1); + expect(component.feedbackDetails).toHaveLength(2); + expect(component.feedbackDetails[0].count).toBe(2); + expect(component.feedbackDetails[1].count).toBe(1); }); }); @@ -133,7 +143,7 @@ describe('TestcaseAnalysisComponent', () => { expect(nonExistingIndex).toBe(0); }); - it('should return -1 if testCaseName is not provided', () => { + it('should return 0 if testCaseName is not provided', () => { const index = component.taskIndex(''); expect(index).toBe(0); }); diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts index 523994e2c371..b93e4dcf18a4 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts @@ -1,16 +1,16 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { FeedbackDetailsWithResultIdsDTO, SimplifiedTask, TestcaseAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.service'; +import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service'; import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; -describe('TestcaseAnalysisService', () => { - let service: TestcaseAnalysisService; +describe('FeedbackAnalysisService', () => { + let service: FeedbackAnalysisService; let httpMock: HttpTestingController; const feedbackDetailsMock: FeedbackDetailsWithResultIdsDTO = { feedbackDetails: [ - { detailText: 'Feedback 1', testCaseName: 'test1' }, - { detailText: 'Feedback 2', testCaseName: 'test2' }, + { detailText: 'Feedback 1', testCaseName: 'test1', count: 0, relativeCount: 0, task: 0 }, + { detailText: 'Feedback 2', testCaseName: 'test2', count: 0, relativeCount: 0, task: 0 }, ], resultIds: [1, 2], }; @@ -23,10 +23,10 @@ describe('TestcaseAnalysisService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [TestcaseAnalysisService], + providers: [FeedbackAnalysisService], }); - service = TestBed.inject(TestcaseAnalysisService); + service = TestBed.inject(FeedbackAnalysisService); httpMock = TestBed.inject(HttpTestingController); }); From 90d0b8ac7205062dc4d6fce890548f51c4b0b19d Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 15:29:37 +0200 Subject: [PATCH 34/52] feedback Ramona/Markus implemented --- ...mming-exercise-configure-grading.component.html | 12 ++++++------ ...ramming-exercise-configure-grading.component.ts | 4 ++-- .../feedback-analysis.component.html | 14 +++++++------- .../feedback-analysis.component.ts | 8 ++++---- .../testcase-analysis/feedback-analysis.service.ts | 4 ++-- src/main/webapp/i18n/de/programmingExercise.json | 6 +++--- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index d4777262c8eb..2485de64f4ba 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -17,8 +17,8 @@

@if (programmingExercise.isAtLeastEditor) { -
- +
+
}
@@ -34,7 +34,7 @@

} - @if (programmingExercise.isAtLeastInstructor && activeTab !== 'testcase-analysis') { + @if (programmingExercise.isAtLeastInstructor && activeTab !== 'feedback-analysis') { }
- @if (activeTab === 'testcase-analysis') { - + > }
} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts index 0d8e0cb1e260..796b2728a4a6 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.ts @@ -56,7 +56,7 @@ const DefaultFieldValues: { [key: string]: number } = { [EditableField.MAX_PENALTY]: 0, }; -export type GradingTab = 'test-cases' | 'code-analysis' | 'submission-policy' | 'testcase-analysis'; +export type GradingTab = 'test-cases' | 'code-analysis' | 'submission-policy' | 'feedback-analysis'; export type Table = 'testCases' | 'codeAnalysis'; @Component({ @@ -231,7 +231,7 @@ export class ProgrammingExerciseConfigureGradingComponent implements OnInit, OnD this.isLoading = false; } - const gradingTabs: GradingTab[] = ['test-cases', 'code-analysis', 'submission-policy', 'testcase-analysis']; + const gradingTabs: GradingTab[] = ['test-cases', 'code-analysis', 'submission-policy', 'feedback-analysis']; if (gradingTabs.includes(params['tab'])) { this.selectTab(params['tab']); } else { diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.html index cc7abf0fe391..06689781b066 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.html @@ -1,13 +1,13 @@
-

+

- - - - - + + + + + @@ -23,5 +23,5 @@

+
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts index e7b0f9f2449b..bf9105ba75ef 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts @@ -15,16 +15,16 @@ export interface FeedbackDetail { } @Component({ - selector: 'jhi-testcase-analysis', + selector: 'jhi-feedback-analysis', templateUrl: './feedback-analysis.component.html', standalone: true, imports: [ArtemisSharedModule], providers: [FeedbackAnalysisService], }) export class FeedbackAnalysisComponent implements OnInit { - @Input() exerciseTitle?: string; - @Input() exerciseId?: number; - @Input() isAtLeastEditor!: undefined | boolean; + @Input() readonly exerciseTitle?: string; + @Input() readonly exerciseId?: number; + @Input() readonly isAtLeastEditor!: undefined | boolean; resultIds: number[] = []; tasks: SimplifiedTask[] = []; feedbackDetails: FeedbackDetail[] = []; diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts index 6a894fcf4403..d279642949e7 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts @@ -17,8 +17,8 @@ export interface FeedbackDetailsWithResultIdsDTO { @Injectable() export class FeedbackAnalysisService { - private resourceUrl = 'api/programming-exercises'; - private exerciseResourceUrl = 'api/exercises'; + private readonly resourceUrl = 'api/programming-exercises'; + private readonly exerciseResourceUrl = 'api/exercises'; constructor(private http: HttpClient) {} diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index b5e526781165..010d3f20f290 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -307,11 +307,11 @@ "testType": "Type", "passedPercent": "Bestanden %" }, - "testAnalysis": { - "titleHeader": "Test Analyse", + "feedbackAnalysis": { + "titleHeader": "Feedback Analyse", "title": "Fehleranalyse für {{exerciseTitle}}", "occurrence": "Häufigkeit", - "testCaseFeedback": "Testfall Feedback", + "feedback": "Feedback", "task": "Aufgabe", "testcase": "Testfall", "errorCategory": "Fehlerkategorie", From 21bc58f659cd4c31bbd4d3cbf36d131e67f02aeb Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 15:39:35 +0200 Subject: [PATCH 35/52] client test fix --- .../feedback-analysis.component.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts index bf9105ba75ef..2645402f4323 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts @@ -1,9 +1,10 @@ import { Component, Input, OnInit } from '@angular/core'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service'; +import { HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { concatMap, tap } from 'rxjs/operators'; -import { HttpResponse } from '@angular/common/http'; + +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service'; import { AlertService } from 'app/core/util/alert.service'; export interface FeedbackDetail { @@ -22,9 +23,9 @@ export interface FeedbackDetail { providers: [FeedbackAnalysisService], }) export class FeedbackAnalysisComponent implements OnInit { - @Input() readonly exerciseTitle?: string; - @Input() readonly exerciseId?: number; - @Input() readonly isAtLeastEditor!: undefined | boolean; + @Input() exerciseTitle?: string; + @Input() exerciseId?: number; + @Input() isAtLeastEditor!: undefined | boolean; resultIds: number[] = []; tasks: SimplifiedTask[] = []; feedbackDetails: FeedbackDetail[] = []; From 44360d6e63fb1ef53402dbcb921cd0c1f9688f08 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 15:43:28 +0200 Subject: [PATCH 36/52] server test fix --- .../www1/artemis/assessment/ResultServiceIntegrationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index db3c06cbdc91..8c67dd795944 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -726,7 +726,7 @@ void testGetAssessmentCountByCorrectionRoundForProgrammingExercise() { } @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetAllFeedbackDetailsForExercise() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); @@ -751,7 +751,7 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { } @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetAllFeedbackDetailsForExercise_NoParticipations() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); From 6d18aa4852df0493d6b5b90e53d80a1648fcc131 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 16:11:10 +0200 Subject: [PATCH 37/52] folder update --- .../feedback-analysis.component.html | 0 .../feedback-analysis.component.ts | 2 +- .../feedback-analysis.service.ts | 2 +- .../manage/grading/programming-exercise-grading.module.ts | 2 +- .../testcase-analysis/testcase-analysis.component.spec.ts | 6 +++--- .../testcase-analysis/testcase-analysis.service.spec.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename src/main/webapp/app/exercises/programming/manage/grading/{testcase-analysis => feedback-analysis}/feedback-analysis.component.html (100%) rename src/main/webapp/app/exercises/programming/manage/grading/{testcase-analysis => feedback-analysis}/feedback-analysis.component.ts (98%) rename src/main/webapp/app/exercises/programming/manage/grading/{testcase-analysis => feedback-analysis}/feedback-analysis.service.ts (96%) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html similarity index 100% rename from src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.html rename to src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts similarity index 98% rename from src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts rename to src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts index 2645402f4323..57634e2969ed 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts @@ -4,7 +4,7 @@ import { Observable } from 'rxjs'; import { concatMap, tap } from 'rxjs/operators'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service'; +import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; import { AlertService } from 'app/core/util/alert.service'; export interface FeedbackDetail { diff --git a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts similarity index 96% rename from src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts rename to src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts index d279642949e7..17a6718924f9 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts @@ -3,7 +3,7 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; -import { FeedbackDetail } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component'; +import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component'; export interface SimplifiedTask { taskName: string; diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts index 1b84f3bb65b6..ea2a8633af73 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-grading.module.ts @@ -19,7 +19,7 @@ import { SubmissionPolicyUpdateModule } from 'app/exercises/shared/submission-po import { ProgrammingExerciseGradingTasksTableComponent } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-grading-tasks-table.component'; import { BarChartModule } from '@swimlane/ngx-charts'; import { ProgrammingExerciseTaskComponent } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task/programming-exercise-task.component'; -import { FeedbackAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component'; +import { FeedbackAnalysisComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component'; @NgModule({ imports: [ diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts index 776217fdcdfc..b1976b616fad 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts @@ -3,10 +3,10 @@ import { firstValueFrom, of, throwError } from 'rxjs'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../test.module'; -import { FeedbackAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component'; -import { FeedbackAnalysisService } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service'; +import { FeedbackAnalysisComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component'; +import { FeedbackAnalysisService } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; import { HttpResponse } from '@angular/common/http'; -import { FeedbackDetail } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.component'; +import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component'; describe('FeedbackAnalysisComponent', () => { let fixture: ComponentFixture; diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts index b93e4dcf18a4..dc7693cc2e45 100644 --- a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/testcase-analysis/feedback-analysis.service'; +import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; describe('FeedbackAnalysisService', () => { From 793332fe3858afb0bce7d6e900d18cd2efc63e6f Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 16:13:45 +0200 Subject: [PATCH 38/52] folder name update --- .../feedback-analysis.component.spec.ts} | 0 .../feedback-analysis.service.spec.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/test/javascript/spec/component/programming-exercise/{testcase-analysis/testcase-analysis.component.spec.ts => feedback-analysis/feedback-analysis.component.spec.ts} (100%) rename src/test/javascript/spec/component/programming-exercise/{testcase-analysis/testcase-analysis.service.spec.ts => feedback-analysis/feedback-analysis.service.spec.ts} (100%) diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts similarity index 100% rename from src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.component.spec.ts rename to src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts diff --git a/src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts similarity index 100% rename from src/test/javascript/spec/component/programming-exercise/testcase-analysis/testcase-analysis.service.spec.ts rename to src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts From 38f4e6df1b1f8a0a81b3bd9a2d476a34deaee9d6 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 16:19:05 +0200 Subject: [PATCH 39/52] removed class from html --- .../programming-exercise-configure-grading.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index 2485de64f4ba..ce9e4e0c609b 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -17,7 +17,7 @@

@if (programmingExercise.isAtLeastEditor) { -
+
} From 1178d8d88226aabb4051aea4f1bed5a213082444 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 16:38:38 +0200 Subject: [PATCH 40/52] removed isAtLeastEditor from component --- .../feedback-analysis.component.ts | 13 ++++--------- ...amming-exercise-configure-grading.component.html | 8 ++------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts index 57634e2969ed..6e5a00c14fe4 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts @@ -25,7 +25,6 @@ export interface FeedbackDetail { export class FeedbackAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; @Input() exerciseId?: number; - @Input() isAtLeastEditor!: undefined | boolean; resultIds: number[] = []; tasks: SimplifiedTask[] = []; feedbackDetails: FeedbackDetail[] = []; @@ -36,14 +35,10 @@ export class FeedbackAnalysisComponent implements OnInit { ) {} ngOnInit(): void { - if (this.isAtLeastEditor) { - if (this.exerciseId) { - this.loadTasks(this.exerciseId) - .pipe(concatMap(() => this.loadFeedbackDetails(this.exerciseId!))) - .subscribe(); - } - } else { - this.alertService.error('Permission Denied'); + if (this.exerciseId) { + this.loadTasks(this.exerciseId) + .pipe(concatMap(() => this.loadFeedbackDetails(this.exerciseId!))) + .subscribe(); } } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index ce9e4e0c609b..bd597d2c0584 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -262,12 +262,8 @@

}

- @if (activeTab === 'feedback-analysis') { - + @if (activeTab === 'feedback-analysis' && programmingExercise.isAtLeastEditor) { + }
} From f7a9e57ebc604aefcc588047339f3b4bd384a7f7 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 16:39:49 +0200 Subject: [PATCH 41/52] removed isAtLeastEditor from component --- .../programming-exercise-configure-grading.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index bd597d2c0584..83d17d06bff1 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -262,7 +262,7 @@

}
- @if (activeTab === 'feedback-analysis' && programmingExercise.isAtLeastEditor) { + @if (programmingExercise.isAtLeastEditor && activeTab === 'feedback-analysis') { }
From 907949305f9c48557d849724417713bc1b1ddece Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 20 Aug 2024 16:44:47 +0200 Subject: [PATCH 42/52] test adjusted --- .../feedback-analysis.component.spec.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts index b1976b616fad..d7b906b5675a 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts @@ -56,7 +56,6 @@ describe('FeedbackAnalysisComponent', () => { describe('ngOnInit', () => { it('should call loadTasks and loadFeedbackDetails when exerciseId is provided', async () => { - component.isAtLeastEditor = true; component.ngOnInit(); await fixture.whenStable(); @@ -73,14 +72,6 @@ describe('FeedbackAnalysisComponent', () => { expect(getSimplifiedTasksSpy).not.toHaveBeenCalled(); expect(getFeedbackDetailsSpy).not.toHaveBeenCalled(); }); - - it('should not call loadTasks and loadFeedbackDetails if user is not at least editor', () => { - component.isAtLeastEditor = false; - component.ngOnInit(); - - expect(getSimplifiedTasksSpy).not.toHaveBeenCalled(); - expect(getFeedbackDetailsSpy).not.toHaveBeenCalled(); - }); }); describe('loadTasks', () => { From 0f40fbd382476432230cc7d8db361c0637eb531e Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 25 Aug 2024 11:17:17 +0200 Subject: [PATCH 43/52] updated performance feedback --- .../StudentParticipationRepository.java | 51 ++++++++ .../www1/artemis/service/ResultService.java | 60 +++++++++- .../www1/artemis/web/rest/ResultResource.java | 37 ++---- .../rest/dto/feedback/FeedbackDetailDTO.java | 2 +- .../FeedbackDetailsWithResultIdsDTO.java | 9 -- .../feedback-analysis.component.html | 2 +- .../feedback-analysis.component.ts | 79 ++----------- .../feedback-analysis.service.ts | 42 ++----- ...-exercise-configure-grading.component.html | 32 +++-- .../ResultServiceIntegrationTest.java | 23 ++-- .../feedback-analysis.component.spec.ts | 110 ++++-------------- 11 files changed, 189 insertions(+), 258 deletions(-) delete mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 8cd15a15b573..1ed4020f848f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -39,6 +39,7 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.domain.quiz.QuizSubmittedAnswerCount; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; +import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailDTO; /** * Spring Data JPA repository for the Participation entity. @@ -1210,4 +1211,54 @@ SELECT COALESCE(AVG(p.presentationScore), 0) AND p.presentationScore IS NOT NULL """) double getAvgPresentationScoreByCourseId(@Param("courseId") long courseId); + + /** + * Get aggregated feedback details for a given exercise, including the count and relative count + * of each unique feedback detail text and test case name. The query considers only the latest + * result for each student participation when calculating the counts and relative counts, and + * excludes participation that are in practice mode (i.e., `testRun` is `TRUE`). + * + * The relative count is calculated as the percentage of the total number of distinct results + * for the exercise, where only the latest automatic result per non-practice participation is considered. + * For the task number, a default value is set as it needs to be determined in a separate step. + * + * @param exerciseId Exercise ID. + * @return a list of FeedbackDetailDTO objects, each containing the feedback count, relative count, + * detail text, test case name, and task number (currently set to 0). + */ + @Query(""" + SELECT new de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailDTO( + COUNT(f.id), + (COUNT(f.id) * 100.0) / (( + SELECT COUNT(DISTINCT r2.id) + FROM StudentParticipation p2 + JOIN p2.results r2 + WHERE p2.exercise.id = :exerciseId + AND r2.assessmentType = de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC + AND r2.id = ( + SELECT MAX(pr2.id) + FROM p2.results pr2 + WHERE pr2.assessmentType = de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC + ) + )), + f.detailText, + tc.testName, + 0 + ) + FROM StudentParticipation p + JOIN p.results r + JOIN r.feedbacks f + LEFT JOIN f.testCase tc + WHERE p.exercise.id = :exerciseId + AND p.testRun = FALSE + AND r.assessmentType = de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC + AND r.id = ( + SELECT MAX(pr.id) + FROM p.results pr + WHERE pr.assessmentType = de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC + ) + AND f.positive = false + GROUP BY f.detailText, tc.testName + """) + List findAggregatedFeedbackByExerciseId(@Param("exerciseId") long exerciseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java index 3ec13cb3d5e0..88141b03e23b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java @@ -34,6 +34,7 @@ import de.tum.in.www1.artemis.domain.enumeration.BuildPlanType; import de.tum.in.www1.artemis.domain.enumeration.FeedbackType; import de.tum.in.www1.artemis.domain.exam.Exam; +import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; @@ -49,11 +50,15 @@ import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.SolutionProgrammingExerciseParticipationRepository; import de.tum.in.www1.artemis.repository.StudentExamRepository; +import de.tum.in.www1.artemis.repository.StudentParticipationRepository; import de.tum.in.www1.artemis.repository.TemplateProgrammingExerciseParticipationRepository; import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseTaskRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.service.connectors.localci.dto.ResultBuildJob; import de.tum.in.www1.artemis.service.connectors.lti.LtiNewResultService; +import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseTaskService; +import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailDTO; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.websocket.ResultWebsocketService; @@ -99,6 +104,10 @@ public class ResultService { private final BuildLogEntryService buildLogEntryService; + private final StudentParticipationRepository studentParticipationRepository; + + private final ProgrammingExerciseTaskService programmingExerciseTaskService; + public ResultService(UserRepository userRepository, ResultRepository resultRepository, Optional ltiNewResultService, ResultWebsocketService resultWebsocketService, ComplaintResponseRepository complaintResponseRepository, RatingRepository ratingRepository, FeedbackRepository feedbackRepository, LongFeedbackTextRepository longFeedbackTextRepository, ComplaintRepository complaintRepository, @@ -106,7 +115,8 @@ public ResultService(UserRepository userRepository, ResultRepository resultRepos TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, StudentExamRepository studentExamRepository, - BuildJobRepository buildJobRepository, BuildLogEntryService buildLogEntryService) { + BuildJobRepository buildJobRepository, BuildLogEntryService buildLogEntryService, StudentParticipationRepository studentParticipationRepository, + ProgrammingExerciseTaskRepository programmingExerciseTaskRepository, ProgrammingExerciseTaskService programmingExerciseTaskService) { this.userRepository = userRepository; this.resultRepository = resultRepository; this.ltiNewResultService = ltiNewResultService; @@ -125,6 +135,8 @@ public ResultService(UserRepository userRepository, ResultRepository resultRepos this.studentExamRepository = studentExamRepository; this.buildJobRepository = buildJobRepository; this.buildLogEntryService = buildLogEntryService; + this.studentParticipationRepository = studentParticipationRepository; + this.programmingExerciseTaskService = programmingExerciseTaskService; } /** @@ -513,4 +525,50 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { return result; } } + + /** + * Retrieves aggregated feedback details for a given exercise, assigning task numbers based on the associated test case names. + * We add the corresponding task number derived from the task and test case mapping to the feedbackDetailDTOs. + * + * @param exerciseId Exercise ID. + * @return a list of FeedbackDetailDTO objects, each containing the feedback count, relative count, + * detail text, test case name, and the determined task number. + */ + public List findAggregatedFeedbackByExerciseId(long exerciseId) { + List tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId).stream().toList(); + List feedbackDetails = studentParticipationRepository.findAggregatedFeedbackByExerciseId(exerciseId); + List updatedFeedbackDetails = new ArrayList<>(); + + for (FeedbackDetailDTO detail : feedbackDetails) { + FeedbackDetailDTO updatedDetail = new FeedbackDetailDTO(detail.count(), detail.relativeCount(), detail.detailText(), detail.testCaseName(), + determineTaskNumber(tasks, detail.testCaseName())); + updatedFeedbackDetails.add(updatedDetail); + } + + return updatedFeedbackDetails; + } + + /** + * Determines the task number associated with a given test case name by iterating through the list of tasks. + * Each task is assigned a number based on its position in the list. If the test case name is found within a task's test cases, + * the corresponding task number is returned. If the test case name is not found, 0 is returned. + * + * @param tasks List of ProgrammingExerciseTask objects containing test cases. + * @param testCaseName The name of the test case for which the task number is to be determined. + * @return the task number associated with the given test case name, or 0 if not found. + */ + private int determineTaskNumber(List tasks, String testCaseName) { + if (testCaseName == null) { + return 0; + } + + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i).getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(testCaseName))) { + return i + 1; + } + } + + return 0; + } + } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 8a9a7ac99a22..2a326e369b5a 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -5,7 +5,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.ZonedDateTime; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -54,7 +53,6 @@ import de.tum.in.www1.artemis.service.exam.ExamDateService; import de.tum.in.www1.artemis.web.rest.dto.ResultWithPointsPerGradingCriterionDTO; import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailDTO; -import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailsWithResultIdsDTO; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; @@ -282,37 +280,18 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo } /** - * GET exercises/:exerciseId/feedback-details : Retrieves all negative feedback details and the latest result IDs for a given exercise. + * GET /exercises/:exerciseId/feedback-details : Retrieves all aggregated feedback details for a given exercise. + * The feedback details include counts and relative counts of feedback occurrences, along with associated test case names and task numbers. * - * @param exerciseId The ID of the exercise for which feedback details and result IDs should be retrieved. - * @return A ResponseEntity containing a list of all feedback details and the corresponding result IDs for the exercise. + * @param exerciseId The ID of the exercise for which feedback details should be retrieved. + * @return A ResponseEntity containing a list of FeedbackDetailDTOs, each with aggregated feedback details, including count, relative count, + * detail text, test case name, and task number. */ - @GetMapping("exercises/{exerciseId}/feedback-details") + @GetMapping("/exercises/{exerciseId}/feedback-details") @EnforceAtLeastEditorInExercise - public ResponseEntity getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { + public ResponseEntity> getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); - - List participations = studentParticipationRepository - .findByExerciseIdWithLatestAutomaticResultAndFeedbacksAndTestCasesWithoutIndividualDueDate(exerciseId); - removeSubmissionAndExerciseData(participations); - - List resultIds = new ArrayList<>(); - - List allFeedbackDetails = new ArrayList<>(participations.stream().filter(participation -> !participation.isPracticeMode()) - .flatMap(participation -> participation.getResults().stream()).peek(result -> resultIds.add(result.getId())).flatMap(result -> result.getFeedbacks().stream()) - .filter(feedback -> Boolean.FALSE.equals(feedback.isPositive())) - .map(feedback -> new FeedbackDetailDTO(feedback.getDetailText(), (feedback.getTestCase() != null ? feedback.getTestCase().getTestName() : null))).toList()); - - FeedbackDetailsWithResultIdsDTO response = new FeedbackDetailsWithResultIdsDTO(allFeedbackDetails, resultIds); - return ResponseEntity.ok(response); - } - - private void removeSubmissionAndExerciseData(List participations) { - // remove unnecessary data to reduce response size - participations.forEach(participation -> { - participation.setSubmissions(null); - participation.setExercise(null); - }); + return ResponseEntity.ok(resultService.findAggregatedFeedbackByExerciseId(exerciseId)); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java index 53f52c85c5ad..d9e1f86d231d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailDTO.java @@ -3,5 +3,5 @@ import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record FeedbackDetailDTO(String detailText, String testCaseName) { +public record FeedbackDetailDTO(long count, double relativeCount, String detailText, String testCaseName, int taskNumber) { } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java deleted file mode 100644 index 49c21870dfa3..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/feedback/FeedbackDetailsWithResultIdsDTO.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.tum.in.www1.artemis.web.rest.dto.feedback; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public record FeedbackDetailsWithResultIdsDTO(List feedbackDetails, List resultIds) { -} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html index 06689781b066..4c76747e8e96 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html @@ -15,7 +15,7 @@

{{ item.count }} ({{ item.relativeCount | number: '1.0-0' }}%)

- + diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts index 6e5a00c14fe4..4ea2eed729d3 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts @@ -1,20 +1,8 @@ import { Component, Input, OnInit } from '@angular/core'; -import { HttpResponse } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { concatMap, tap } from 'rxjs/operators'; - +import { FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; import { AlertService } from 'app/core/util/alert.service'; -export interface FeedbackDetail { - count: number; - relativeCount: number; - detailText: string; - testCaseName: string; - task: number; -} - @Component({ selector: 'jhi-feedback-analysis', templateUrl: './feedback-analysis.component.html', @@ -25,75 +13,24 @@ export interface FeedbackDetail { export class FeedbackAnalysisComponent implements OnInit { @Input() exerciseTitle?: string; @Input() exerciseId?: number; - resultIds: number[] = []; - tasks: SimplifiedTask[] = []; feedbackDetails: FeedbackDetail[] = []; constructor( - private simplifiedProgrammingExerciseTaskService: FeedbackAnalysisService, + private feedbackAnalysisService: FeedbackAnalysisService, private alertService: AlertService, ) {} ngOnInit(): void { if (this.exerciseId) { - this.loadTasks(this.exerciseId) - .pipe(concatMap(() => this.loadFeedbackDetails(this.exerciseId!))) - .subscribe(); + this.loadFeedbackDetails(this.exerciseId); } } - loadTasks(exerciseId: number): Observable { - return this.simplifiedProgrammingExerciseTaskService.getSimplifiedTasks(exerciseId).pipe( - tap((tasks) => { - this.tasks = tasks; - }), - ); - } - - loadFeedbackDetails(exerciseId: number): Observable> { - return this.simplifiedProgrammingExerciseTaskService.getFeedbackDetailsForExercise(exerciseId).pipe( - tap((response) => { - this.resultIds = response.body?.resultIds || []; - const feedbackDetails = response.body?.feedbackDetails || []; - this.saveFeedback(feedbackDetails); - }), - ); - } - - saveFeedback(feedbackDetails: FeedbackDetail[]): void { - const feedbackMap: Map = new Map(); - - feedbackDetails.forEach((feedback) => { - const feedbackText = feedback.detailText ?? ''; - const testCaseName = feedback.testCaseName ?? ''; - const key = `${feedbackText}_${testCaseName}`; - - const existingFeedback = feedbackMap.get(key); - if (existingFeedback) { - existingFeedback.count += 1; - existingFeedback.relativeCount = this.getRelativeCount(existingFeedback.count); - } else { - const task = this.taskIndex(testCaseName); - feedbackMap.set(key, { - count: 1, - relativeCount: this.getRelativeCount(1), - detailText: feedbackText, - testCaseName: testCaseName, - task: task, - }); - } - }); - this.feedbackDetails = Array.from(feedbackMap.values()).sort((a, b) => b.count - a.count); - } - - taskIndex(testCaseName: string): number { - if (!testCaseName) { - return 0; + async loadFeedbackDetails(exerciseId: number): Promise { + try { + this.feedbackDetails = await this.feedbackAnalysisService.getFeedbackDetailsForExercise(exerciseId); + } catch (error) { + this.alertService.error('An error occurred while loading feedback details.'); } - return this.tasks.findIndex((tasks) => tasks.testCases?.some((testCase) => testCase.testName === testCaseName)) + 1; - } - - getRelativeCount(count: number): number { - return (count / this.resultIds.length) * 100; } } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts index 17a6718924f9..4fa81cf289d3 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts @@ -1,39 +1,19 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpResponse } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; -import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component'; +import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api-http.service'; -export interface SimplifiedTask { - taskName: string; - testCases: ProgrammingExerciseServerSideTask['testCases']; -} - -export interface FeedbackDetailsWithResultIdsDTO { - feedbackDetails: FeedbackDetail[]; - resultIds: number[]; +export interface FeedbackDetail { + count: number; + relativeCount: number; + detailText: string; + testCaseName: string; + taskNumber: number; } @Injectable() -export class FeedbackAnalysisService { - private readonly resourceUrl = 'api/programming-exercises'; - private readonly exerciseResourceUrl = 'api/exercises'; - - constructor(private http: HttpClient) {} - - getFeedbackDetailsForExercise(exerciseId: number): Observable> { - return this.http.get(`${this.exerciseResourceUrl}/${exerciseId}/feedback-details`, { observe: 'response' }); - } +export class FeedbackAnalysisService extends BaseApiHttpService { + private readonly EXERCISE_RESOURCE_URL = 'exercises'; - public getSimplifiedTasks(exerciseId: number): Observable { - return this.http.get(`${this.resourceUrl}/${exerciseId}/tasks-with-unassigned-test-cases`).pipe( - map((tasks) => - tasks.map((task) => ({ - taskName: task.taskName ?? '', - testCases: task.testCases ?? [], - })), - ), - ); + getFeedbackDetailsForExercise(exerciseId: number): Promise { + return this.get(`${this.EXERCISE_RESOURCE_URL}/${exerciseId}/feedback-details`); } } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index 83d17d06bff1..1781947b886c 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -5,23 +5,31 @@

-
- -
+ @if (programmingExercise.staticCodeAnalysisEnabled) { -
- -
+ } -
- -
+ @if (programmingExercise.isAtLeastEditor) { -
- -
+ }
+ +
+ +
+
diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index 8c67dd795944..9a2ae9bd929d 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -73,7 +73,6 @@ import de.tum.in.www1.artemis.repository.TextExerciseRepository; import de.tum.in.www1.artemis.web.rest.dto.ResultWithPointsPerGradingCriterionDTO; import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailDTO; -import de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailsWithResultIdsDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; class ResultServiceIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { @@ -739,27 +738,25 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { feedback.setTestCase(null); participationUtilService.addFeedbackToResult(feedback, result); - FeedbackDetailsWithResultIdsDTO response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, - FeedbackDetailsWithResultIdsDTO.class); + List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); - assertThat(response.feedbackDetails()).isNotEmpty(); - assertThat(response.resultIds()).containsExactly(result.getId()); + assertThat(response).isNotEmpty(); - FeedbackDetailDTO feedbackDetail = response.feedbackDetails().get(0); + FeedbackDetailDTO feedbackDetail = response.getFirst(); + assertThat(feedbackDetail.count()).isEqualTo(1); + assertThat(feedbackDetail.relativeCount()).isNotNull(); assertThat(feedbackDetail.detailText()).isEqualTo("Some feedback"); - assertThat(feedbackDetail.testCaseName()).isNull(); // Test case name should be null since no test case was linked + assertThat(feedbackDetail.testCaseName()).isNull(); + assertThat(feedbackDetail.taskNumber()).isEqualTo(0); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGetAllFeedbackDetailsForExercise_NoParticipations() throws Exception { + void testGetAllFeedbackDetailsForExercise_NoParticipation() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); + List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); - FeedbackDetailsWithResultIdsDTO response = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, - FeedbackDetailsWithResultIdsDTO.class); - - assertThat(response.feedbackDetails()).isNull(); - assertThat(response.resultIds()).isNull(); + assertThat(response).isEmpty(); } } diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts index d7b906b5675a..8ee13a07e902 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts @@ -1,38 +1,26 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { firstValueFrom, of, throwError } from 'rxjs'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../test.module'; import { FeedbackAnalysisComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component'; import { FeedbackAnalysisService } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; -import { HttpResponse } from '@angular/common/http'; -import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component'; +import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; describe('FeedbackAnalysisComponent', () => { let fixture: ComponentFixture; let component: FeedbackAnalysisComponent; - let testcaseAnalysisService: FeedbackAnalysisService; - let getSimplifiedTasksSpy: jest.SpyInstance; + let feedbackAnalysisService: FeedbackAnalysisService; let getFeedbackDetailsSpy: jest.SpyInstance; const feedbackMock: FeedbackDetail[] = [ - { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 0, relativeCount: 0, task: 0 }, - { detailText: 'Test feedback 2 detail', testCaseName: 'test2', count: 0, relativeCount: 0, task: 0 }, - { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 0, relativeCount: 0, task: 0 }, + { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 10, relativeCount: 50, taskNumber: 1 }, + { detailText: 'Test feedback 2 detail', testCaseName: 'test2', count: 5, relativeCount: 25, taskNumber: 2 }, ]; - const tasksMock = [ - { taskName: 'Task 1', testCases: [{ testName: 'test1' }] }, - { taskName: 'Task 2', testCases: [{ testName: 'test2' }] }, - ]; - - const feedbackDetailsResponseMock = new HttpResponse({ - body: { feedbackDetails: feedbackMock, resultIds: [1, 2] }, - }); - - beforeEach(() => { - return TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [ArtemisTestModule, TranslateModule.forRoot(), FeedbackAnalysisComponent], + declarations: [], providers: [ { provide: TranslateService, @@ -40,103 +28,45 @@ describe('FeedbackAnalysisComponent', () => { }, FeedbackAnalysisService, ], - }) - .compileComponents() - .then(() => { - fixture = TestBed.createComponent(FeedbackAnalysisComponent); - component = fixture.componentInstance; - component.exerciseId = 1; - - testcaseAnalysisService = fixture.debugElement.injector.get(FeedbackAnalysisService); - - getSimplifiedTasksSpy = jest.spyOn(testcaseAnalysisService, 'getSimplifiedTasks').mockReturnValue(of(tasksMock)); - getFeedbackDetailsSpy = jest.spyOn(testcaseAnalysisService, 'getFeedbackDetailsForExercise').mockReturnValue(of(feedbackDetailsResponseMock)); - }); + }).compileComponents(); + fixture = TestBed.createComponent(FeedbackAnalysisComponent); + component = fixture.componentInstance; + component.exerciseId = 1; + feedbackAnalysisService = fixture.debugElement.injector.get(FeedbackAnalysisService); + getFeedbackDetailsSpy = jest.spyOn(feedbackAnalysisService, 'getFeedbackDetailsForExercise').mockResolvedValue(feedbackMock); }); describe('ngOnInit', () => { - it('should call loadTasks and loadFeedbackDetails when exerciseId is provided', async () => { + it('should call loadFeedbackDetails when exerciseId is provided', async () => { component.ngOnInit(); await fixture.whenStable(); - expect(getSimplifiedTasksSpy).toHaveBeenCalledWith(1); expect(getFeedbackDetailsSpy).toHaveBeenCalledWith(1); - expect(component.tasks).toEqual(tasksMock); - expect(component.resultIds).toEqual([1, 2]); + expect(component.feedbackDetails).toEqual(feedbackMock); }); - it('should not call loadTasks and loadFeedbackDetails if exerciseId is not provided', () => { + it('should not call loadFeedbackDetails if exerciseId is not provided', async () => { component.exerciseId = undefined; component.ngOnInit(); - expect(getSimplifiedTasksSpy).not.toHaveBeenCalled(); expect(getFeedbackDetailsSpy).not.toHaveBeenCalled(); }); }); - describe('loadTasks', () => { - it('should load tasks and update the component state', async () => { - await firstValueFrom(component.loadTasks(1)); - expect(component.tasks).toEqual(tasksMock); - }); - - it('should handle error while loading tasks', async () => { - getSimplifiedTasksSpy.mockReturnValue(throwError(() => new Error('Error loading tasks'))); - - try { - await firstValueFrom(component.loadTasks(1)); - } catch { - expect(component.tasks).toEqual([]); - } - }); - }); - describe('loadFeedbackDetails', () => { it('should load feedback details and update the component state', async () => { - await firstValueFrom(component.loadFeedbackDetails(1)); - expect(component.resultIds).toEqual([1, 2]); - expect(component.feedbackDetails).toHaveLength(2); + await component.loadFeedbackDetails(1); + expect(component.feedbackDetails).toEqual(feedbackMock); }); it('should handle error while loading feedback details', async () => { - getFeedbackDetailsSpy.mockReturnValue(throwError(() => new Error('Error loading feedback details'))); + getFeedbackDetailsSpy.mockRejectedValue(new Error('Error loading feedback details')); try { - await firstValueFrom(component.loadFeedbackDetails(1)); + await component.loadFeedbackDetails(1); } catch { expect(component.feedbackDetails).toEqual([]); - expect(component.resultIds).toEqual([]); } }); }); - - describe('saveFeedback', () => { - it('should save feedbacks and sort them by count', () => { - component.saveFeedback(feedbackMock); - - expect(component.feedbackDetails).toHaveLength(2); - expect(component.feedbackDetails[0].count).toBe(2); - expect(component.feedbackDetails[1].count).toBe(1); - }); - }); - - describe('taskIndex', () => { - it('should find the correct task index for a given test case', () => { - component.tasks = tasksMock; - - const index1 = component.taskIndex('test1'); - expect(index1).toBe(1); - - const index2 = component.taskIndex('test2'); - expect(index2).toBe(2); - - const nonExistingIndex = component.taskIndex('non-existing'); - expect(nonExistingIndex).toBe(0); - }); - - it('should return 0 if testCaseName is not provided', () => { - const index = component.taskIndex(''); - expect(index).toBe(0); - }); - }); }); From 420519088ddcddfbae24cafe904663465b762673 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 25 Aug 2024 13:30:34 +0200 Subject: [PATCH 44/52] service tests updated --- .../feedback-analysis.service.spec.ts | 65 ++++--------------- 1 file changed, 12 insertions(+), 53 deletions(-) diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts index dc7693cc2e45..a4f9a2a3ee42 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts @@ -1,23 +1,14 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { FeedbackAnalysisService, FeedbackDetailsWithResultIdsDTO, SimplifiedTask } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; -import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programming-exercise-task.model'; +import { FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; describe('FeedbackAnalysisService', () => { let service: FeedbackAnalysisService; let httpMock: HttpTestingController; - const feedbackDetailsMock: FeedbackDetailsWithResultIdsDTO = { - feedbackDetails: [ - { detailText: 'Feedback 1', testCaseName: 'test1', count: 0, relativeCount: 0, task: 0 }, - { detailText: 'Feedback 2', testCaseName: 'test2', count: 0, relativeCount: 0, task: 0 }, - ], - resultIds: [1, 2], - }; - - const simplifiedTasksMock: ProgrammingExerciseServerSideTask[] = [ - { taskName: 'Task 1', testCases: [{ testName: 'test1' }] as ProgrammingExerciseServerSideTask['testCases'] }, - { taskName: 'Task 2', testCases: [{ testName: 'test2' }] as ProgrammingExerciseServerSideTask['testCases'] }, + const feedbackDetailsMock: FeedbackDetail[] = [ + { detailText: 'Feedback 1', testCaseName: 'test1', count: 5, relativeCount: 25.0, taskNumber: 1 }, + { detailText: 'Feedback 2', testCaseName: 'test2', count: 3, relativeCount: 15.0, taskNumber: 2 }, ]; beforeEach(() => { @@ -35,57 +26,25 @@ describe('FeedbackAnalysisService', () => { }); describe('getFeedbackDetailsForExercise', () => { - it('should retrieve feedback details for a given exercise', () => { - service.getFeedbackDetailsForExercise(1).subscribe((response) => { - expect(response.body).toEqual(feedbackDetailsMock); - }); + it('should retrieve feedback details for a given exercise', async () => { + const responsePromise = service.getFeedbackDetailsForExercise(1); const req = httpMock.expectOne('api/exercises/1/feedback-details'); expect(req.request.method).toBe('GET'); req.flush(feedbackDetailsMock); + + const result = await responsePromise; + expect(result).toEqual(feedbackDetailsMock); }); - it('should handle errors while retrieving feedback details', () => { - service.getFeedbackDetailsForExercise(1).subscribe({ - next: () => {}, - error: (error) => { - expect(error.status).toBe(500); - }, - }); + it('should handle errors while retrieving feedback details', async () => { + const responsePromise = service.getFeedbackDetailsForExercise(1); const req = httpMock.expectOne('api/exercises/1/feedback-details'); expect(req.request.method).toBe('GET'); req.flush('Something went wrong', { status: 500, statusText: 'Server Error' }); - }); - }); - - describe('getSimplifiedTasks', () => { - it('should retrieve simplified tasks for a given exercise', () => { - const expectedTasks: SimplifiedTask[] = [ - { taskName: 'Task 1', testCases: [{ testName: 'test1' }] }, - { taskName: 'Task 2', testCases: [{ testName: 'test2' }] }, - ]; - - service.getSimplifiedTasks(1).subscribe((tasks) => { - expect(tasks).toEqual(expectedTasks); - }); - const req = httpMock.expectOne('api/programming-exercises/1/tasks-with-unassigned-test-cases'); - expect(req.request.method).toBe('GET'); - req.flush(simplifiedTasksMock); - }); - - it('should handle errors while retrieving simplified tasks', () => { - service.getSimplifiedTasks(1).subscribe({ - next: () => {}, - error: (error) => { - expect(error.status).toBe(404); - }, - }); - - const req = httpMock.expectOne('api/programming-exercises/1/tasks-with-unassigned-test-cases'); - expect(req.request.method).toBe('GET'); - req.flush('Not Found', { status: 404, statusText: 'Not Found' }); + await expect(responsePromise).rejects.toThrow('Internal server error'); }); }); }); From 1336f1ec8cbe74dddc4d1af011219a5b600ebaa6 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 25 Aug 2024 13:39:08 +0200 Subject: [PATCH 45/52] server style --- .../www1/artemis/repository/StudentParticipationRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 1ed4020f848f..69147ad1e7dd 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -1257,7 +1257,7 @@ SELECT MAX(pr.id) FROM p.results pr WHERE pr.assessmentType = de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC ) - AND f.positive = false + AND f.positive = FALSE GROUP BY f.detailText, tc.testName """) List findAggregatedFeedbackByExerciseId(@Param("exerciseId") long exerciseId); From 3f67553a2050d271cf0989a1a596f141406446b6 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 25 Aug 2024 18:52:11 +0200 Subject: [PATCH 46/52] adjusted performance even more --- .../StudentParticipationRepository.java | 79 +++++++++---------- .../www1/artemis/service/ResultService.java | 59 +++++--------- .../www1/artemis/web/rest/ResultResource.java | 2 +- .../feedback-analysis.component.ts | 4 +- ...-exercise-configure-grading.component.html | 12 +-- .../ResultServiceIntegrationTest.java | 13 +-- .../feedback-analysis.component.spec.ts | 2 +- 7 files changed, 77 insertions(+), 94 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 69147ad1e7dd..fa08cce57e8a 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -1213,52 +1213,51 @@ SELECT COALESCE(AVG(p.presentationScore), 0) double getAvgPresentationScoreByCourseId(@Param("courseId") long courseId); /** - * Get aggregated feedback details for a given exercise, including the count and relative count - * of each unique feedback detail text and test case name. The query considers only the latest - * result for each student participation when calculating the counts and relative counts, and - * excludes participation that are in practice mode (i.e., `testRun` is `TRUE`). - * - * The relative count is calculated as the percentage of the total number of distinct results - * for the exercise, where only the latest automatic result per non-practice participation is considered. - * For the task number, a default value is set as it needs to be determined in a separate step. + * Retrieves aggregated feedback details for a given exercise, including the count of each unique feedback detail text and test case name. + *
+ * The relative count and task number are initially set to 0 and are calculated in a separate step in the service layer. * * @param exerciseId Exercise ID. - * @return a list of FeedbackDetailDTO objects, each containing the feedback count, relative count, - * detail text, test case name, and task number (currently set to 0). + * @return a list of {@link FeedbackDetailDTO} objects, with the relative count and task number set to 0. */ @Query(""" - SELECT new de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailDTO( - COUNT(f.id), - (COUNT(f.id) * 100.0) / (( - SELECT COUNT(DISTINCT r2.id) - FROM StudentParticipation p2 - JOIN p2.results r2 - WHERE p2.exercise.id = :exerciseId - AND r2.assessmentType = de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC - AND r2.id = ( - SELECT MAX(pr2.id) - FROM p2.results pr2 - WHERE pr2.assessmentType = de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC - ) - )), - f.detailText, - tc.testName, - 0 + SELECT new de.tum.in.www1.artemis.web.rest.dto.feedback.FeedbackDetailDTO( + COUNT(f.id), + 0, + f.detailText, + f.testCase.testName, + 0 ) + FROM StudentParticipation p + JOIN p.results r + JOIN r.feedbacks f + WHERE p.exercise.id = :exerciseId + AND p.testRun = FALSE + AND r.id = ( + SELECT MAX(pr.id) + FROM p.results pr + ) + AND f.positive = FALSE + GROUP BY f.detailText, f.testCase.testName + """) + List findAggregatedFeedbackByExerciseId(@Param("exerciseId") long exerciseId); + + /** + * Counts the distinct number of latest results for a given exercise, excluding those in practice mode. + * + * @param exerciseId Exercise ID. + * @return The count of distinct latest results for the exercise. + */ + @Query(""" + SELECT COUNT(DISTINCT r.id) FROM StudentParticipation p - JOIN p.results r - JOIN r.feedbacks f - LEFT JOIN f.testCase tc + JOIN p.results r WHERE p.exercise.id = :exerciseId - AND p.testRun = FALSE - AND r.assessmentType = de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC - AND r.id = ( - SELECT MAX(pr.id) - FROM p.results pr - WHERE pr.assessmentType = de.tum.in.www1.artemis.domain.enumeration.AssessmentType.AUTOMATIC - ) - AND f.positive = FALSE - GROUP BY f.detailText, tc.testName + AND p.testRun = FALSE + AND r.id = ( + SELECT MAX(pr.id) + FROM p.results pr + ) """) - List findAggregatedFeedbackByExerciseId(@Param("exerciseId") long exerciseId); + long countDistinctResultsByExerciseId(@Param("exerciseId") long exerciseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java index 88141b03e23b..8ef069e026b5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java @@ -527,48 +527,31 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { } /** - * Retrieves aggregated feedback details for a given exercise, assigning task numbers based on the associated test case names. - * We add the corresponding task number derived from the task and test case mapping to the feedbackDetailDTOs. + * Retrieves aggregated feedback details for a given exercise, calculating relative counts based on the total number of distinct results. + * The task numbers are assigned based on the associated test case names, using the set of tasks fetched from the database. + *
+ * For each feedback detail: + * 1. The relative count is calculated as a percentage of the total number of distinct results for the exercise. + * 2. The task number is determined by matching the test case name with the tasks. * - * @param exerciseId Exercise ID. - * @return a list of FeedbackDetailDTO objects, each containing the feedback count, relative count, - * detail text, test case name, and the determined task number. + * @param exerciseId The ID of the exercise for which feedback details should be retrieved. + * @return A list of FeedbackDetailDTO objects, each containing: + * - feedback count, + * - relative count (as a percentage of distinct results), + * - detail text, + * - test case name, + * - determined task number (based on the test case name). */ public List findAggregatedFeedbackByExerciseId(long exerciseId) { - List tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId).stream().toList(); + long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId); + Set tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); List feedbackDetails = studentParticipationRepository.findAggregatedFeedbackByExerciseId(exerciseId); - List updatedFeedbackDetails = new ArrayList<>(); - for (FeedbackDetailDTO detail : feedbackDetails) { - FeedbackDetailDTO updatedDetail = new FeedbackDetailDTO(detail.count(), detail.relativeCount(), detail.detailText(), detail.testCaseName(), - determineTaskNumber(tasks, detail.testCaseName())); - updatedFeedbackDetails.add(updatedDetail); - } - - return updatedFeedbackDetails; + return feedbackDetails.stream().map(detail -> { + double relativeCount = (detail.count() * 100.0) / distinctResultCount; + int taskNumber = tasks.stream().filter(task -> task.getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(detail.testCaseName()))).findFirst() + .map(task -> tasks.stream().toList().indexOf(task) + 1).orElse(0); + return new FeedbackDetailDTO(detail.count(), relativeCount, detail.detailText(), detail.testCaseName(), taskNumber); + }).collect(Collectors.toList()); } - - /** - * Determines the task number associated with a given test case name by iterating through the list of tasks. - * Each task is assigned a number based on its position in the list. If the test case name is found within a task's test cases, - * the corresponding task number is returned. If the test case name is not found, 0 is returned. - * - * @param tasks List of ProgrammingExerciseTask objects containing test cases. - * @param testCaseName The name of the test case for which the task number is to be determined. - * @return the task number associated with the given test case name, or 0 if not found. - */ - private int determineTaskNumber(List tasks, String testCaseName) { - if (testCaseName == null) { - return 0; - } - - for (int i = 0; i < tasks.size(); i++) { - if (tasks.get(i).getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(testCaseName))) { - return i + 1; - } - } - - return 0; - } - } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 2a326e369b5a..e6797d88d8d1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -287,7 +287,7 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo * @return A ResponseEntity containing a list of FeedbackDetailDTOs, each with aggregated feedback details, including count, relative count, * detail text, test case name, and task number. */ - @GetMapping("/exercises/{exerciseId}/feedback-details") + @GetMapping("exercises/{exerciseId}/feedback-details") @EnforceAtLeastEditorInExercise public ResponseEntity> getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts index 4ea2eed729d3..369dd78d19bf 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts @@ -11,8 +11,8 @@ import { AlertService } from 'app/core/util/alert.service'; providers: [FeedbackAnalysisService], }) export class FeedbackAnalysisComponent implements OnInit { - @Input() exerciseTitle?: string; - @Input() exerciseId?: number; + @Input() exerciseTitle: string; + @Input() exerciseId: number; feedbackDetails: FeedbackDetail[] = []; constructor( diff --git a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html index 1781947b886c..2eba9de15c39 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/programming-exercise-configure-grading.component.html @@ -1,3 +1,8 @@ + +
+ +
+

@@ -25,11 +30,6 @@

-
- -
-
@@ -271,7 +271,7 @@

@if (programmingExercise.isAtLeastEditor && activeTab === 'feedback-analysis') { - + }
} diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index 9a2ae9bd929d..32449e7d7d22 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -33,6 +33,7 @@ import de.tum.in.www1.artemis.domain.GradingCriterion; import de.tum.in.www1.artemis.domain.GradingInstruction; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.domain.Result; import de.tum.in.www1.artemis.domain.Submission; @@ -729,25 +730,25 @@ void testGetAssessmentCountByCorrectionRoundForProgrammingExercise() { void testGetAllFeedbackDetailsForExercise() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); - Result result = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation); + ProgrammingExerciseTestCase testCase = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "test1"); + testCase.setId(1L); Feedback feedback = new Feedback(); feedback.setPositive(false); feedback.setDetailText("Some feedback"); - feedback.setTestCase(null); + feedback.setTestCase(testCase); participationUtilService.addFeedbackToResult(feedback, result); List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); assertThat(response).isNotEmpty(); - FeedbackDetailDTO feedbackDetail = response.getFirst(); assertThat(feedbackDetail.count()).isEqualTo(1); - assertThat(feedbackDetail.relativeCount()).isNotNull(); + assertThat(feedbackDetail.relativeCount()).isEqualTo(100.0); assertThat(feedbackDetail.detailText()).isEqualTo("Some feedback"); - assertThat(feedbackDetail.testCaseName()).isNull(); - assertThat(feedbackDetail.taskNumber()).isEqualTo(0); + assertThat(feedbackDetail.testCaseName()).isEqualTo("test1"); + assertThat(feedbackDetail.taskNumber()).isEqualTo(1); } @Test diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts index 8ee13a07e902..4b4cdf919ea9 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts @@ -45,7 +45,7 @@ describe('FeedbackAnalysisComponent', () => { expect(component.feedbackDetails).toEqual(feedbackMock); }); - it('should not call loadFeedbackDetails if exerciseId is not provided', async () => { + it('should not call loadFeedbackDetails if exerciseId is not provided', () => { component.exerciseId = undefined; component.ngOnInit(); From d13ee4dd7e90e84a2e5e9026f16a96e33038a0fd Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 25 Aug 2024 18:53:26 +0200 Subject: [PATCH 47/52] adjusted performance even more --- .../feedback-analysis/feedback-analysis.component.ts | 4 +--- .../feedback-analysis/feedback-analysis.component.spec.ts | 7 ------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts index 369dd78d19bf..acdc5473bee1 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts @@ -21,9 +21,7 @@ export class FeedbackAnalysisComponent implements OnInit { ) {} ngOnInit(): void { - if (this.exerciseId) { - this.loadFeedbackDetails(this.exerciseId); - } + this.loadFeedbackDetails(this.exerciseId); } async loadFeedbackDetails(exerciseId: number): Promise { diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts index 4b4cdf919ea9..0e9387b93e5b 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts @@ -44,13 +44,6 @@ describe('FeedbackAnalysisComponent', () => { expect(getFeedbackDetailsSpy).toHaveBeenCalledWith(1); expect(component.feedbackDetails).toEqual(feedbackMock); }); - - it('should not call loadFeedbackDetails if exerciseId is not provided', () => { - component.exerciseId = undefined; - component.ngOnInit(); - - expect(getFeedbackDetailsSpy).not.toHaveBeenCalled(); - }); }); describe('loadFeedbackDetails', () => { From e295a19e67ca023e4d7fdbc6d4ec34cdb68b1486 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 25 Aug 2024 21:00:11 +0200 Subject: [PATCH 48/52] adjusted performance even more --- .../StudentParticipationRepository.java | 19 ------------------- .../www1/artemis/service/ResultService.java | 3 +-- .../www1/artemis/web/rest/ResultResource.java | 1 - 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index fa08cce57e8a..04dec3d419b3 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -1241,23 +1241,4 @@ SELECT MAX(pr.id) GROUP BY f.detailText, f.testCase.testName """) List findAggregatedFeedbackByExerciseId(@Param("exerciseId") long exerciseId); - - /** - * Counts the distinct number of latest results for a given exercise, excluding those in practice mode. - * - * @param exerciseId Exercise ID. - * @return The count of distinct latest results for the exercise. - */ - @Query(""" - SELECT COUNT(DISTINCT r.id) - FROM StudentParticipation p - JOIN p.results r - WHERE p.exercise.id = :exerciseId - AND p.testRun = FALSE - AND r.id = ( - SELECT MAX(pr.id) - FROM p.results pr - ) - """) - long countDistinctResultsByExerciseId(@Param("exerciseId") long exerciseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java index 8ef069e026b5..fcf50e4e3037 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java @@ -543,12 +543,11 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { * - determined task number (based on the test case name). */ public List findAggregatedFeedbackByExerciseId(long exerciseId) { - long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId); Set tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); List feedbackDetails = studentParticipationRepository.findAggregatedFeedbackByExerciseId(exerciseId); return feedbackDetails.stream().map(detail -> { - double relativeCount = (detail.count() * 100.0) / distinctResultCount; + double relativeCount = (detail.count() * 100.0) / feedbackDetails.size(); int taskNumber = tasks.stream().filter(task -> task.getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(detail.testCaseName()))).findFirst() .map(task -> tasks.stream().toList().indexOf(task) + 1).orElse(0); return new FeedbackDetailDTO(detail.count(), relativeCount, detail.detailText(), detail.testCaseName(), taskNumber); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index e6797d88d8d1..51472dfb8468 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -293,5 +293,4 @@ public ResponseEntity> getAllFeedbackDetailsForExercise( log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); return ResponseEntity.ok(resultService.findAggregatedFeedbackByExerciseId(exerciseId)); } - } From aca6ffaa7bb09c6f690805791d05daa0b8790700 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 25 Aug 2024 21:07:04 +0200 Subject: [PATCH 49/52] fixed calculation --- .../StudentParticipationRepository.java | 19 +++++++++++++++++++ .../www1/artemis/service/ResultService.java | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 04dec3d419b3..fa08cce57e8a 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -1241,4 +1241,23 @@ SELECT MAX(pr.id) GROUP BY f.detailText, f.testCase.testName """) List findAggregatedFeedbackByExerciseId(@Param("exerciseId") long exerciseId); + + /** + * Counts the distinct number of latest results for a given exercise, excluding those in practice mode. + * + * @param exerciseId Exercise ID. + * @return The count of distinct latest results for the exercise. + */ + @Query(""" + SELECT COUNT(DISTINCT r.id) + FROM StudentParticipation p + JOIN p.results r + WHERE p.exercise.id = :exerciseId + AND p.testRun = FALSE + AND r.id = ( + SELECT MAX(pr.id) + FROM p.results pr + ) + """) + long countDistinctResultsByExerciseId(@Param("exerciseId") long exerciseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java index fcf50e4e3037..8ef069e026b5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java @@ -543,11 +543,12 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { * - determined task number (based on the test case name). */ public List findAggregatedFeedbackByExerciseId(long exerciseId) { + long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId); Set tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); List feedbackDetails = studentParticipationRepository.findAggregatedFeedbackByExerciseId(exerciseId); return feedbackDetails.stream().map(detail -> { - double relativeCount = (detail.count() * 100.0) / feedbackDetails.size(); + double relativeCount = (detail.count() * 100.0) / distinctResultCount; int taskNumber = tasks.stream().filter(task -> task.getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(detail.testCaseName()))).findFirst() .map(task -> tasks.stream().toList().indexOf(task) + 1).orElse(0); return new FeedbackDetailDTO(detail.count(), relativeCount, detail.detailText(), detail.testCaseName(), taskNumber); From ff845481b161066fc556203442946d70c3afaf84 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 25 Aug 2024 22:04:20 +0200 Subject: [PATCH 50/52] feedback implemented --- .../www1/artemis/web/rest/ResultResource.java | 3 +- .../feedback-analysis.component.ts | 2 +- .../webapp/i18n/de/programmingExercise.json | 3 +- .../webapp/i18n/en/programmingExercise.json | 3 +- .../ResultServiceIntegrationTest.java | 51 +++++++++++++++++++ 5 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java index 51472dfb8468..bd901ccd824b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ResultResource.java @@ -284,8 +284,7 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo * The feedback details include counts and relative counts of feedback occurrences, along with associated test case names and task numbers. * * @param exerciseId The ID of the exercise for which feedback details should be retrieved. - * @return A ResponseEntity containing a list of FeedbackDetailDTOs, each with aggregated feedback details, including count, relative count, - * detail text, test case name, and task number. + * @return A ResponseEntity containing a list of {@link FeedbackDetailDTO}s */ @GetMapping("exercises/{exerciseId}/feedback-details") @EnforceAtLeastEditorInExercise diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts index acdc5473bee1..7e1d48121f1c 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts @@ -28,7 +28,7 @@ export class FeedbackAnalysisComponent implements OnInit { try { this.feedbackDetails = await this.feedbackAnalysisService.getFeedbackDetailsForExercise(exerciseId); } catch (error) { - this.alertService.error('An error occurred while loading feedback details.'); + this.alertService.error(`artemisApp.programmingExercise.configureGrading.feedbackAnalysis.error`); } } } diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 010d3f20f290..9dd7b92cec8d 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -315,7 +315,8 @@ "task": "Aufgabe", "testcase": "Testfall", "errorCategory": "Fehlerkategorie", - "totalItems": "Insgesamt {{count}} Elemente" + "totalItems": "Insgesamt {{count}} Elemente", + "error": "Ein Fehler ist beim Laden des Feedback aufgetreten." }, "help": { "name": "Aufgabennamen werden fett geschrieben, während Testnamen normal sind. Ob es ein Aufgabenname oder Testname ist hängt davon ab, ob die Reihe eine Aufgabe oder einen Test darstellt.", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 647214a7c3fb..570879bbe125 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -317,7 +317,8 @@ "task": "Task", "testcase": "Test Case", "errorCategory": "Error Category", - "totalItems": "In total {{count}} items" + "totalItems": "In total {{count}} items", + "error": "An error occurred while loading the feedback." }, "help": { "name": "Task names are written in bold whereas Test names are normal. Task or test name depending on whether the row is a task or test.", diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index 32449e7d7d22..2c8e7c82b389 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -751,6 +751,57 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { assertThat(feedbackDetail.taskNumber()).isEqualTo(1); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception { + ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); + StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); + StudentParticipation participation2 = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student2"); + Result result = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation); + Result result2 = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation2); + ProgrammingExerciseTestCase testCase = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "test1"); + testCase.setId(1L); + + Feedback feedback1 = new Feedback(); + feedback1.setPositive(false); + feedback1.setDetailText("Some feedback"); + feedback1.setTestCase(testCase); + participationUtilService.addFeedbackToResult(feedback1, result); + + Feedback feedback2 = new Feedback(); + feedback2.setPositive(false); + feedback2.setDetailText("Some feedback"); + feedback2.setTestCase(testCase); + participationUtilService.addFeedbackToResult(feedback2, result2); + + Feedback feedback3 = new Feedback(); + feedback3.setPositive(false); + feedback3.setDetailText("Some different feedback"); + feedback3.setTestCase(testCase); + participationUtilService.addFeedbackToResult(feedback3, result); + + List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); + + assertThat(response).hasSize(2); + + FeedbackDetailDTO firstFeedbackDetail = response.stream().filter(feedbackDetail -> "Some feedback".equals(feedbackDetail.detailText())).findFirst().orElseThrow(); + + FeedbackDetailDTO secondFeedbackDetail = response.stream().filter(feedbackDetail -> "Some different feedback".equals(feedbackDetail.detailText())).findFirst() + .orElseThrow(); + + assertThat(firstFeedbackDetail.count()).isEqualTo(2); + assertThat(firstFeedbackDetail.relativeCount()).isEqualTo(100.0); + assertThat(firstFeedbackDetail.detailText()).isEqualTo("Some feedback"); + assertThat(firstFeedbackDetail.testCaseName()).isEqualTo("test1"); + assertThat(firstFeedbackDetail.taskNumber()).isEqualTo(1); + + assertThat(secondFeedbackDetail.count()).isEqualTo(1); + assertThat(secondFeedbackDetail.relativeCount()).isEqualTo(50.0); + assertThat(secondFeedbackDetail.detailText()).isEqualTo("Some different feedback"); + assertThat(secondFeedbackDetail.testCaseName()).isEqualTo("test1"); + assertThat(secondFeedbackDetail.taskNumber()).isEqualTo(1); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetAllFeedbackDetailsForExercise_NoParticipation() throws Exception { From ed8e1a60cb74247cdc9e2c3aab5d6bead3eb11dc Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Wed, 28 Aug 2024 18:05:47 +0200 Subject: [PATCH 51/52] server style --- src/main/java/de/tum/in/www1/artemis/service/ResultService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java index 8ef069e026b5..139a3b01b01c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ResultService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ResultService.java @@ -552,6 +552,6 @@ public List findAggregatedFeedbackByExerciseId(long exerciseI int taskNumber = tasks.stream().filter(task -> task.getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(detail.testCaseName()))).findFirst() .map(task -> tasks.stream().toList().indexOf(task) + 1).orElse(0); return new FeedbackDetailDTO(detail.count(), relativeCount, detail.detailText(), detail.testCaseName(), taskNumber); - }).collect(Collectors.toList()); + }).toList(); } } From d2970f6b8e516c396374cbd04b52901a0f458b3b Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Thu, 29 Aug 2024 09:31:17 +0200 Subject: [PATCH 52/52] translation file --- src/main/webapp/i18n/de/programmingExercise.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 9dd7b92cec8d..ca4d45bb1486 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -316,7 +316,7 @@ "testcase": "Testfall", "errorCategory": "Fehlerkategorie", "totalItems": "Insgesamt {{count}} Elemente", - "error": "Ein Fehler ist beim Laden des Feedback aufgetreten." + "error": "Beim Laden des Feedback ist ein Fehler aufgetreten." }, "help": { "name": "Aufgabennamen werden fett geschrieben, während Testnamen normal sind. Ob es ein Aufgabenname oder Testname ist hängt davon ab, ob die Reihe eine Aufgabe oder einen Test darstellt.",

{{ item.detailText }}{{ item.task }}{{ item.taskNumber }} {{ item.testCaseName }} Student Error