From 0b1023e51b0b0293d2a7edbe1c08a63c0927fe38 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 13 Sep 2024 12:37:21 +0200 Subject: [PATCH 01/16] first approach --- .../Modal/feedback-modal.component.html | 29 +++++ .../Modal/feedback-modal.component.scss | 29 +++++ .../Modal/feedback-modal.component.ts | 17 +++ .../feedback-analysis.component.html | 74 ++++++++++-- .../feedback-analysis.component.scss | 13 +++ .../feedback-analysis.component.ts | 110 +++++++++++++++--- .../webapp/i18n/de/programmingExercise.json | 23 ++-- .../webapp/i18n/en/programmingExercise.json | 23 ++-- 8 files changed, 262 insertions(+), 56 deletions(-) create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.scss create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.scss diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html new file mode 100644 index 000000000000..25ab8fdda6c4 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html @@ -0,0 +1,29 @@ + + + diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.scss b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.scss new file mode 100644 index 000000000000..f3acbf865cd8 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.scss @@ -0,0 +1,29 @@ +.modal-body { + padding: 1rem; +} + +.modal-label { + font-weight: bold; +} + +.border { + padding: 5px; + border-left: var(--border-color) 1px solid; + border-right: var(--border-color) 1px solid; + border-bottom: var(--border-color) 1px solid; + border-radius: 10px; +} + +.modal-header { + border-bottom: none; + padding-left: 1rem; + padding-top: 1.5rem; +} + +.modal-body p { + margin-bottom: 0; +} + +.modal-footer { + border-top: none; +} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts new file mode 100644 index 000000000000..4a5d7807f4f9 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; + +@Component({ + selector: 'jhi-feedback-modal', + templateUrl: './feedback-modal.component.html', + styleUrls: ['./feedback-modal.component.scss'], + imports: [ArtemisSharedCommonModule], + standalone: true, +}) +export class FeedbackModalComponent { + @Input() feedbackDetail!: FeedbackDetail; + + constructor(public activeModal: NgbActiveModal) {} +} 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 4c76747e8e96..224be21af28b 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 @@ -1,27 +1,81 @@ + + + + @if (sortedColumn() === column) { + @if (sortingOrder() === 'ASC') { + + } @else { + + } + } + + +
-

+

+ +
+ + +
+ - - - - - + + + + + - @for (item of feedbackDetails; track item) { + @for (item of paginatedFeedbackDetails(); track item) { - + - + }
{{ item.count }} ({{ item.relativeCount | number: '1.0-0' }}%){{ item.detailText }} + {{ item.detailText.length > 150 ? (item.detailText | slice: 0 : 100) + '...' : item.detailText }} + {{ item.taskNumber }} {{ item.testCaseName }} Student Error + +
-
+ +
+ + +
+ +
+
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.scss b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.scss new file mode 100644 index 000000000000..2c35828b698f --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.scss @@ -0,0 +1,13 @@ +.small-text { + font-size: 0.8rem; +} + +.position-relative { + position: relative; +} + +.search-icon { + position: absolute; + right: 10px; + pointer-events: none; +} 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 7e1d48121f1c..6413829e88e8 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,34 +1,116 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, InputSignal, computed, effect, inject, input, signal } from '@angular/core'; import { FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; import { AlertService } from 'app/core/util/alert.service'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { faMagnifyingGlass, faMagnifyingGlassPlus, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; +import { FeedbackModalComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component'; +import { SortService } from 'app/shared/service/sort.service'; + +enum SortingOrder { + ASCENDING = 'ASC', + DESCENDING = 'DESC', +} @Component({ selector: 'jhi-feedback-analysis', templateUrl: './feedback-analysis.component.html', + styleUrls: ['./feedback-analysis.component.scss'], standalone: true, - imports: [ArtemisSharedModule], + imports: [ArtemisSharedCommonModule], providers: [FeedbackAnalysisService], }) -export class FeedbackAnalysisComponent implements OnInit { - @Input() exerciseTitle: string; - @Input() exerciseId: number; - feedbackDetails: FeedbackDetail[] = []; +export class FeedbackAnalysisComponent { + exerciseTitle: InputSignal = input.required(); + exerciseId: InputSignal = input.required(); + + private feedbackAnalysisService = inject(FeedbackAnalysisService); + private alertService = inject(AlertService); + private modalService = inject(NgbModal); + private sortService = inject(SortService); + + readonly feedbackDetails = signal([]); + + readonly sortedColumn = signal('count'); + readonly sortingOrder = signal(SortingOrder.DESCENDING); + readonly faSort = faSort; + readonly faSortUp = faSortUp; + readonly faSortDown = faSortDown; + readonly faMagnifyingGlass = faMagnifyingGlass; + readonly faMagnifyingGlassPlus = faMagnifyingGlassPlus; + + readonly page = signal(1); + readonly pageSize = signal(15); + readonly searchTerm = signal(''); + + readonly paginatedFeedbackDetails = computed(() => { + const filteredAndSorted = this.getFilteredAndSortedFeedback(); + const start = (this.page() - 1) * this.pageSize(); + const end = start + this.pageSize(); + return filteredAndSorted.slice(start, end); + }); - constructor( - private feedbackAnalysisService: FeedbackAnalysisService, - private alertService: AlertService, - ) {} + readonly collectionSize = computed(() => this.getFilteredAndSortedFeedback().length); - ngOnInit(): void { - this.loadFeedbackDetails(this.exerciseId); + constructor() { + effect(() => { + this.loadFeedbackDetails(this.exerciseId()); + }); } async loadFeedbackDetails(exerciseId: number): Promise { try { - this.feedbackDetails = await this.feedbackAnalysisService.getFeedbackDetailsForExercise(exerciseId); + this.feedbackDetails.set(await this.feedbackAnalysisService.getFeedbackDetailsForExercise(exerciseId)); } catch (error) { this.alertService.error(`artemisApp.programmingExercise.configureGrading.feedbackAnalysis.error`); } } + + private getFilteredAndSortedFeedback() { + const searchTermLower = this.searchTerm().toLowerCase(); + const searchTermNumber = Number(this.searchTerm()); + const hasNumber = !isNaN(searchTermNumber); + + const filtered = this.filterForSearch(searchTermLower, hasNumber, searchTermNumber); + return this.sortFeedbackDetails(filtered); + } + + private sortFeedbackDetails(details: FeedbackDetail[]): FeedbackDetail[] { + const column = this.sortedColumn(); + const order = this.sortingOrder() === SortingOrder.ASCENDING; + return this.sortService.sortByProperty(details, column, order); + } + + private filterForSearch(searchTermLower: string, hasNumber: boolean, searchTermNumber: number) { + return this.feedbackDetails().filter((item) => { + const matchesTextFields = item.detailText.toLowerCase().includes(searchTermLower) || item.testCaseName.toLowerCase().includes(searchTermLower); + + const matchesNumericFields = hasNumber && (item.taskNumber === searchTermNumber || item.count === searchTermNumber || item.relativeCount === searchTermNumber); + + return matchesTextFields || matchesNumericFields; + }); + } + + setPage(newPage: number): void { + this.page.set(newPage); + } + + search(): void { + this.page.set(1); + } + + setSortedColumn(column: string): void { + if (this.sortedColumn() === column) { + this.sortingOrder.set(this.sortingOrder() === SortingOrder.ASCENDING ? SortingOrder.DESCENDING : SortingOrder.ASCENDING); + } else { + this.sortedColumn.set(column); + this.sortingOrder.set(SortingOrder.ASCENDING); + } + this.page.set(1); + } + + openFeedbackModal(feedbackDetail: FeedbackDetail): void { + const modalRef = this.modalService.open(FeedbackModalComponent, { centered: true }); + modalRef.componentInstance.feedbackDetail = feedbackDetail; + } } diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 4e493f7d4daf..6cfe36a77c3c 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -118,22 +118,14 @@ "workdir": "Verzeichnis", "allowOnlineEditor": { "title": "Online-Editor erlauben", - "alert": "Es muss mindestens eine Option (Offline-IDE, Online-Editor oder Online-IDE) ausgewählt sein", - "alertNoTheia": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" + "alert": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" }, "onlineEditor": "Online", "allowOfflineIde": { "title": "Offline-IDE erlauben", - "alert": "Es muss mindestens eine Option (Offline-IDE, Online-Editor oder Online-IDE) ausgewählt sein", - "alertNoTheia": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" + "alert": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" }, "offlineIde": "IDE", - "allowOnlineIde": { - "title": "Online-IDE erlauben", - "alert": "Es muss mindestens eine Option (Offline-IDE, Online-Editor oder Online-IDE) ausgewählt sein.", - "alertNoTheia": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" - }, - "onlineIde": "Online IDE", "showTestNamesToStudents": "Zeige die Test Namen den Studierenden", "showTestNamesToStudentsTooltip": "Durch Aktivierung dieser Option werden die Namen der automatischen Tests den Studierenden angezeigt. Lasse die Option deaktiviert, um keine visuelle Unterscheidung zwischen manuellem und automatischem Feedback für die Studierenden vorzunehmen.", "participationMode": "Teilnahmemodus", @@ -178,11 +170,6 @@ "projectType": "Projekttyp", "testRepositoryProjectType": "Projekttyp des Test-Repository", "packageName": "Package-Name", - "theiaImage": { - "title": "Konfiguration für Online IDE", - "noImageAvailable": "Die Online IDE ist für diese Programmiersprache noch nicht verfügbar.", - "alert": "Es muss eine gültige Konfiguration für die Online IDE ausgewählt werden." - }, "appName": "App-Name", "templateResult": "Ergebnis der Vorlage", "solutionResult": "Ergebnis der Musterlösung", @@ -329,7 +316,11 @@ "testcase": "Testfall", "errorCategory": "Fehlerkategorie", "totalItems": "Insgesamt {{count}} Elemente", - "error": "Beim Laden des Feedback ist ein Fehler aufgetreten." + "error": "Beim Laden des Feedback ist ein Fehler aufgetreten.", + "feedbackModal": { + "header": "Fehler Details", + "feedbackTitle": "Testfall Feedback" + } }, "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 1883d8294abc..7a3c8ff71bf7 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -130,22 +130,14 @@ "customizeDockerImage": "You can customize the Docker image. Make sure to provide it in amd64 and arm64 and include all build dependencies to guarantee a short build duration.", "allowOnlineEditor": { "title": "Allow Online Editor", - "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected", - "alertNoTheia": "At least one option (Offline IDE or Online Editor) must be selected" + "alert": "At least one option (Offline IDE or Online Editor) must be selected" }, "onlineEditor": "Online", "allowOfflineIde": { "title": "Allow Offline IDE", - "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected", - "alertNoTheia": "At least one option (Offline IDE or Online Editor) must be selected" + "alert": "At least one option (Offline IDE or Online Editor) must be selected" }, "offlineIde": "IDE", - "allowOnlineIde": { - "title": "Allow Online IDE", - "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected.", - "alertNoTheia": "At least one option (Offline IDE or Online Editor) must be selected" - }, - "onlineIde": "Online IDE", "showTestNamesToStudents": "Show Test Names to Students", "showTestNamesToStudentsTooltip": "Activate this option to show the names of the automated test cases to the students. Leave the option disabled to make no visual distinction between manual and automated feedback for the students.", "participationMode": "Participation Mode", @@ -180,11 +172,6 @@ "projectType": "Project Type", "testRepositoryProjectType": "Test Repository Project Type", "packageName": "Package Name", - "theiaImage": { - "title": "Configuration for Online IDE", - "noImageAvailable": "The Online IDE is not yet available for this programming language.", - "alert": "A valid configuration for the Online IDE must be selected." - }, "appName": "App Name", "templateResult": "Template Result", "solutionResult": "Solution Result", @@ -331,7 +318,11 @@ "testcase": "Test Case", "errorCategory": "Error Category", "totalItems": "In total {{count}} items", - "error": "An error occurred while loading the feedback." + "error": "An error occurred while loading the feedback.", + "feedbackModal": { + "header": "Error Details", + "feedbackTitle": "Test Case 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.", From e14b18c8d108e4d2edf380541e410ab770a45ae8 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 13 Sep 2024 18:33:08 +0200 Subject: [PATCH 02/16] first working valid approach --- .../dto/FeedbackAnalysisResponseDTO.java | 9 ++ .../assessment/service/ResultService.java | 41 ++++++- .../assessment/web/ResultResource.java | 10 +- .../feedback-analysis.component.html | 11 +- .../feedback-analysis.component.ts | 114 ++++++++---------- .../feedback-analysis.service.ts | 27 ++++- 6 files changed, 128 insertions(+), 84 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java new file mode 100644 index 000000000000..060af704bcb2 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java @@ -0,0 +1,9 @@ +package de.tum.cit.aet.artemis.assessment.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record FeedbackAnalysisResponseDTO(SearchResultPageDTO feedbackDetails, long distinctResultCount) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java index 66a042ffea0e..e2caa33fc0ff 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java @@ -28,6 +28,7 @@ import de.tum.cit.aet.artemis.assessment.domain.FeedbackType; import de.tum.cit.aet.artemis.assessment.domain.LongFeedbackText; import de.tum.cit.aet.artemis.assessment.domain.Result; +import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO; import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO; import de.tum.cit.aet.artemis.assessment.repository.ComplaintRepository; import de.tum.cit.aet.artemis.assessment.repository.ComplaintResponseRepository; @@ -40,6 +41,9 @@ import de.tum.cit.aet.artemis.buildagent.dto.ResultBuildJob; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; +import de.tum.cit.aet.artemis.core.dto.SortingOrder; +import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; @@ -545,16 +549,47 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { * - test case name, * - determined task number (based on the test case name). */ - public List findAggregatedFeedbackByExerciseId(long exerciseId) { + public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, SearchTermPageableSearchDTO search) { + // Step 1: Retrieve the distinct result count and the tasks long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId); Set tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); + + // Step 2: Retrieve all feedback details for the exercise List feedbackDetails = studentParticipationRepository.findAggregatedFeedbackByExerciseId(exerciseId); + long totalFeedbackCount = feedbackDetails.size(); - return feedbackDetails.stream().map(detail -> { + // Step 3 & 4: Calculate relative count, task number, and filter in a single step + String searchTerm = search.getSearchTerm() != null ? search.getSearchTerm().toLowerCase() : ""; + feedbackDetails = 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); - }).toList(); + }).filter(detail -> searchTerm.isEmpty() || detail.detailText().toLowerCase().contains(searchTerm) || detail.testCaseName().toLowerCase().contains(searchTerm) + || String.valueOf(detail.count()).contains(searchTerm) || String.valueOf(detail.taskNumber()).contains(searchTerm) + || String.valueOf(detail.relativeCount()).contains(searchTerm)).collect(Collectors.toList()); + + // Step 5: Apply sorting + feedbackDetails.sort((a, b) -> { + int comparison = switch (search.getSortedColumn()) { + case "count" -> Long.compare(a.count(), b.count()); + case "detailText" -> a.detailText().compareToIgnoreCase(b.detailText()); + case "testCaseName" -> a.testCaseName().compareToIgnoreCase(b.testCaseName()); + case "taskNumber" -> Integer.compare(a.taskNumber(), b.taskNumber()); + case "relativeCount" -> Double.compare(a.relativeCount(), b.relativeCount()); + default -> 0; + }; + return search.getSortingOrder() == SortingOrder.ASCENDING ? comparison : -comparison; + }); + + // Step 6: Apply pagination + int start = (search.getPage() - 1) * search.getPageSize(); + int end = Math.min(start + search.getPageSize(), feedbackDetails.size()); + List paginatedFeedbackDetails = feedbackDetails.subList(start, end); + + // Step 7: Calculate total pages + int totalPages = (feedbackDetails.size() + search.getPageSize() - 1) / search.getPageSize(); + return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(paginatedFeedbackDetails, totalPages), totalFeedbackCount); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java index ea8f2fcaa290..3d9a98b8ac0b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java @@ -27,12 +27,14 @@ import de.tum.cit.aet.artemis.assessment.domain.Feedback; import de.tum.cit.aet.artemis.assessment.domain.Result; +import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO; import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO; import de.tum.cit.aet.artemis.assessment.dto.ResultWithPointsPerGradingCriterionDTO; import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; import de.tum.cit.aet.artemis.assessment.service.ResultService; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; @@ -286,10 +288,10 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo * @param exerciseId The ID of the exercise for which feedback details should be retrieved. * @return A ResponseEntity containing a list of {@link FeedbackDetailDTO}s */ - @GetMapping("exercises/{exerciseId}/feedback-details") + @PostMapping("/exercises/{exerciseId}/feedback-details-paged") @EnforceAtLeastEditorInExercise - public ResponseEntity> getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { - log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); - return ResponseEntity.ok(resultService.findAggregatedFeedbackByExerciseId(exerciseId)); + public ResponseEntity getFeedbackDetailsPaged(@PathVariable long exerciseId, @RequestBody SearchTermPageableSearchDTO search) { + FeedbackAnalysisResponseDTO response = resultService.getFeedbackDetailsOnPage(exerciseId, search); + return ResponseEntity.ok(response); } } 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 224be21af28b..e489766d0717 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 @@ -2,7 +2,7 @@ @if (sortedColumn() === column) { - @if (sortingOrder() === 'ASC') { + @if (sortingOrder() === SortingOrder.ASCENDING) { } @else { @@ -43,7 +43,7 @@

- @for (item of paginatedFeedbackDetails(); track item) { + @for (item of content().resultsOnPage; track item) { {{ item.count }} ({{ item.relativeCount | number: '1.0-0' }}%) @@ -62,19 +62,20 @@

+ > +
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 6413829e88e8..c0b1b66f4432 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,16 +1,11 @@ -import { Component, InputSignal, computed, effect, inject, input, signal } from '@angular/core'; -import { FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; -import { AlertService } from 'app/core/util/alert.service'; -import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { Component, InputSignal, effect, inject, input, signal } from '@angular/core'; +import { FeedbackAnalysisResponse, FeedbackAnalysisService, FeedbackDetail } from './feedback-analysis.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { AlertService } from 'app/core/util/alert.service'; import { faMagnifyingGlass, faMagnifyingGlassPlus, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; +import { SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; import { FeedbackModalComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component'; -import { SortService } from 'app/shared/service/sort.service'; - -enum SortingOrder { - ASCENDING = 'ASC', - DESCENDING = 'DESC', -} @Component({ selector: 'jhi-feedback-analysis', @@ -24,79 +19,59 @@ export class FeedbackAnalysisComponent { exerciseTitle: InputSignal = input.required(); exerciseId: InputSignal = input.required(); - private feedbackAnalysisService = inject(FeedbackAnalysisService); + // Signals for reactive state + readonly page = signal(1); + readonly pageSize = signal(15); + searchTerm = signal(''); // Initially empty + readonly sortingOrder = signal(SortingOrder.DESCENDING); + readonly sortedColumn = signal('count'); + + readonly isLoading = signal(false); + readonly content = signal>({ resultsOnPage: [], numberOfPages: 0 }); + distinctResultCount = signal(0); // To store the distinct result count + + // Inject dependencies + private pagingService = inject(FeedbackAnalysisService); private alertService = inject(AlertService); private modalService = inject(NgbModal); - private sortService = inject(SortService); - - readonly feedbackDetails = signal([]); - readonly sortedColumn = signal('count'); - readonly sortingOrder = signal(SortingOrder.DESCENDING); readonly faSort = faSort; readonly faSortUp = faSortUp; readonly faSortDown = faSortDown; readonly faMagnifyingGlass = faMagnifyingGlass; readonly faMagnifyingGlassPlus = faMagnifyingGlassPlus; - readonly page = signal(1); - readonly pageSize = signal(15); - readonly searchTerm = signal(''); - - readonly paginatedFeedbackDetails = computed(() => { - const filteredAndSorted = this.getFilteredAndSortedFeedback(); - const start = (this.page() - 1) * this.pageSize(); - const end = start + this.pageSize(); - return filteredAndSorted.slice(start, end); - }); - - readonly collectionSize = computed(() => this.getFilteredAndSortedFeedback().length); - constructor() { effect(() => { - this.loadFeedbackDetails(this.exerciseId()); + this.loadData(); // This will be triggered immediately upon page load }); } - async loadFeedbackDetails(exerciseId: number): Promise { - try { - this.feedbackDetails.set(await this.feedbackAnalysisService.getFeedbackDetailsForExercise(exerciseId)); - } catch (error) { - this.alertService.error(`artemisApp.programmingExercise.configureGrading.feedbackAnalysis.error`); - } - } - - private getFilteredAndSortedFeedback() { - const searchTermLower = this.searchTerm().toLowerCase(); - const searchTermNumber = Number(this.searchTerm()); - const hasNumber = !isNaN(searchTermNumber); - - const filtered = this.filterForSearch(searchTermLower, hasNumber, searchTermNumber); - return this.sortFeedbackDetails(filtered); - } - - private sortFeedbackDetails(details: FeedbackDetail[]): FeedbackDetail[] { - const column = this.sortedColumn(); - const order = this.sortingOrder() === SortingOrder.ASCENDING; - return this.sortService.sortByProperty(details, column, order); - } - - private filterForSearch(searchTermLower: string, hasNumber: boolean, searchTermNumber: number) { - return this.feedbackDetails().filter((item) => { - const matchesTextFields = item.detailText.toLowerCase().includes(searchTermLower) || item.testCaseName.toLowerCase().includes(searchTermLower); - - const matchesNumericFields = hasNumber && (item.taskNumber === searchTermNumber || item.count === searchTermNumber || item.relativeCount === searchTermNumber); - - return matchesTextFields || matchesNumericFields; - }); + private loadData(): void { + const state = { + page: this.page(), + pageSize: this.pageSize(), + searchTerm: this.searchTerm(), // Will be empty initially + sortingOrder: this.sortingOrder(), + sortedColumn: this.sortedColumn(), + }; + + this.pagingService + .search(state, { exerciseId: this.exerciseId() }) // Make sure exerciseId is correct + .subscribe({ + next: (response: FeedbackAnalysisResponse) => { + this.content.set(response.feedbackDetails); + this.distinctResultCount.set(response.distinctResultCount); // Store distinct result count + }, + error: (error) => { + this.alertService.error(error.message); + }, + }); } setPage(newPage: number): void { this.page.set(newPage); - } - - search(): void { - this.page.set(1); + this.loadData(); } setSortedColumn(column: string): void { @@ -106,11 +81,18 @@ export class FeedbackAnalysisComponent { this.sortedColumn.set(column); this.sortingOrder.set(SortingOrder.ASCENDING); } - this.page.set(1); + this.loadData(); + } + + search(): void { + this.page.set(1); // Reset to page 1 when searching + this.loadData(); } openFeedbackModal(feedbackDetail: FeedbackDetail): void { const modalRef = this.modalService.open(FeedbackModalComponent, { centered: true }); modalRef.componentInstance.feedbackDetail = feedbackDetail; } + + protected readonly SortingOrder = SortingOrder; } 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 4fa81cf289d3..01b9a9afe150 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,5 +1,9 @@ +import { HttpClient, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api-http.service'; +import { PagingService } from 'app/exercises/shared/manage/paging.service'; +import { SearchResult, SearchTermPageableSearch } from 'app/shared/table/pageable-table'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; export interface FeedbackDetail { count: number; @@ -9,11 +13,22 @@ export interface FeedbackDetail { taskNumber: number; } -@Injectable() -export class FeedbackAnalysisService extends BaseApiHttpService { - private readonly EXERCISE_RESOURCE_URL = 'exercises'; +export interface FeedbackAnalysisResponse { + feedbackDetails: SearchResult; + distinctResultCount: number; +} + +@Injectable({ providedIn: 'root' }) +export class FeedbackAnalysisService extends PagingService { + private resourceUrl = 'api'; + + constructor(private http: HttpClient) { + super(); + } - getFeedbackDetailsForExercise(exerciseId: number): Promise { - return this.get(`${this.EXERCISE_RESOURCE_URL}/${exerciseId}/feedback-details`); + override search(pageable: SearchTermPageableSearch, options: { exerciseId: number }): Observable { + return this.http + .post(`${this.resourceUrl}/exercises/${options.exerciseId}/feedback-details-paged`, pageable, { observe: 'response' }) + .pipe(map((resp: HttpResponse) => resp.body!)); } } From 8e4c04616ee93cf7a9f700475803b3f453eb1d5e Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 13 Sep 2024 19:44:57 +0200 Subject: [PATCH 03/16] final approach and server side cleaned with server side tests adapted --- .../assessment/service/ResultService.java | 69 ++++++++++--------- .../assessment/web/ResultResource.java | 12 ++-- .../feedback-analysis.component.ts | 18 ++--- .../feedback-analysis.service.ts | 27 +++----- .../ResultServiceIntegrationTest.java | 58 ++++++++++++---- 5 files changed, 103 insertions(+), 81 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java index e2caa33fc0ff..a6ff3e57ab4d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java @@ -12,7 +12,9 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.IntStream; import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; @@ -534,62 +536,61 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { } /** - * Retrieves aggregated feedback details for a given exercise, calculating relative counts based on the total number of distinct results. + * Retrieves paginated and filtered 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. + *
+ * The method supports filtering by a search term across feedback details, test case names, counts, task numbers, and relative counts. + * Sorting is applied based on the specified column and order (ascending or descending). + * The result is paginated based on the provided page number and page size. * * @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). + * @param search The pageable search DTO containing page number, page size, sorting options, and a search term for filtering results. + * @return A {@link FeedbackAnalysisResponseDTO} object containing: + * - a {@link SearchResultPageDTO} of paginated feedback details, and + * - the total number of distinct results (distinctResultCount) for the exercise. */ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, SearchTermPageableSearchDTO search) { - // Step 1: Retrieve the distinct result count and the tasks long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId); Set tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); - // Step 2: Retrieve all feedback details for the exercise List feedbackDetails = studentParticipationRepository.findAggregatedFeedbackByExerciseId(exerciseId); + String searchTerm = search.getSearchTerm() != null ? search.getSearchTerm().toLowerCase() : ""; + long totalFeedbackCount = feedbackDetails.size(); - // Step 3 & 4: Calculate relative count, task number, and filter in a single step - String searchTerm = search.getSearchTerm() != null ? search.getSearchTerm().toLowerCase() : ""; + Predicate matchesSearchTerm = detail -> searchTerm.isEmpty() || detail.detailText().toLowerCase().contains(searchTerm) + || detail.testCaseName().toLowerCase().contains(searchTerm) || String.valueOf(detail.count()).contains(searchTerm) + || String.valueOf(detail.taskNumber()).contains(searchTerm) || String.valueOf(detail.relativeCount()).contains(searchTerm); + feedbackDetails = 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); + int taskNumber = IntStream.range(0, tasks.size()) + .filter(i -> tasks.stream().toList().get(i).getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(detail.testCaseName()))).findFirst().orElse(-1) + 1; return new FeedbackDetailDTO(detail.count(), relativeCount, detail.detailText(), detail.testCaseName(), taskNumber); - }).filter(detail -> searchTerm.isEmpty() || detail.detailText().toLowerCase().contains(searchTerm) || detail.testCaseName().toLowerCase().contains(searchTerm) - || String.valueOf(detail.count()).contains(searchTerm) || String.valueOf(detail.taskNumber()).contains(searchTerm) - || String.valueOf(detail.relativeCount()).contains(searchTerm)).collect(Collectors.toList()); - - // Step 5: Apply sorting - feedbackDetails.sort((a, b) -> { - int comparison = switch (search.getSortedColumn()) { - case "count" -> Long.compare(a.count(), b.count()); - case "detailText" -> a.detailText().compareToIgnoreCase(b.detailText()); - case "testCaseName" -> a.testCaseName().compareToIgnoreCase(b.testCaseName()); - case "taskNumber" -> Integer.compare(a.taskNumber(), b.taskNumber()); - case "relativeCount" -> Double.compare(a.relativeCount(), b.relativeCount()); - default -> 0; - }; - return search.getSortingOrder() == SortingOrder.ASCENDING ? comparison : -comparison; - }); + }).filter(matchesSearchTerm).collect(Collectors.toList()); + + Map> comparators = Map.of("count", Comparator.comparingLong(FeedbackDetailDTO::count), "detailText", + Comparator.comparing(FeedbackDetailDTO::detailText, String.CASE_INSENSITIVE_ORDER), "testCaseName", + Comparator.comparing(FeedbackDetailDTO::testCaseName, String.CASE_INSENSITIVE_ORDER), "taskNumber", Comparator.comparingInt(FeedbackDetailDTO::taskNumber), + "relativeCount", Comparator.comparingDouble(FeedbackDetailDTO::relativeCount)); + + Comparator comparator = comparators.getOrDefault(search.getSortedColumn(), (a, b) -> 0); + feedbackDetails.sort(search.getSortingOrder() == SortingOrder.ASCENDING ? comparator : comparator.reversed()); + + int pageSize = search.getPageSize(); + int page = search.getPage(); + + int start = (page - 1) * pageSize; + int end = Math.min(start + pageSize, feedbackDetails.size()); - // Step 6: Apply pagination - int start = (search.getPage() - 1) * search.getPageSize(); - int end = Math.min(start + search.getPageSize(), feedbackDetails.size()); List paginatedFeedbackDetails = feedbackDetails.subList(start, end); - // Step 7: Calculate total pages - int totalPages = (feedbackDetails.size() + search.getPageSize() - 1) / search.getPageSize(); + int totalPages = (feedbackDetails.size() + pageSize - 1) / pageSize; return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(paginatedFeedbackDetails, totalPages), totalFeedbackCount); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java index 3d9a98b8ac0b..a69b3c583014 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java @@ -28,7 +28,6 @@ import de.tum.cit.aet.artemis.assessment.domain.Feedback; import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO; -import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO; import de.tum.cit.aet.artemis.assessment.dto.ResultWithPointsPerGradingCriterionDTO; import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; import de.tum.cit.aet.artemis.assessment.service.ResultService; @@ -282,11 +281,16 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo } /** - * 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. + * POST /exercises/{exerciseId}/feedback-details-paged : Retrieves paginated and filtered aggregated feedback details for a given exercise. + * The feedback details include counts and relative counts of feedback occurrences, test case names, and task numbers. + * The method allows filtering by search term and sorting by various fields. + *
+ * Pagination is applied based on the provided {@link SearchTermPageableSearchDTO}, including page number, page size, sorting order, and search term. + * The response contains both the paginated feedback details and the total count of distinct results for the exercise. * * @param exerciseId The ID of the exercise for which feedback details should be retrieved. - * @return A ResponseEntity containing a list of {@link FeedbackDetailDTO}s + * @param search The pageable search DTO containing page number, page size, sorting options, and a search term for filtering results. + * @return A {@link ResponseEntity} containing a {@link FeedbackAnalysisResponseDTO}, which includes the paginated feedback details and the total count of distinct results. */ @PostMapping("/exercises/{exerciseId}/feedback-details-paged") @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 c0b1b66f4432..50bb67c53b29 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,5 +1,5 @@ import { Component, InputSignal, effect, inject, input, signal } from '@angular/core'; -import { FeedbackAnalysisResponse, FeedbackAnalysisService, FeedbackDetail } from './feedback-analysis.service'; +import { FeedbackAnalysisService, FeedbackDetail } from './feedback-analysis.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { AlertService } from 'app/core/util/alert.service'; import { faMagnifyingGlass, faMagnifyingGlassPlus, faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; @@ -47,7 +47,7 @@ export class FeedbackAnalysisComponent { }); } - private loadData(): void { + private async loadData(): Promise { const state = { page: this.page(), pageSize: this.pageSize(), @@ -56,17 +56,9 @@ export class FeedbackAnalysisComponent { sortedColumn: this.sortedColumn(), }; - this.pagingService - .search(state, { exerciseId: this.exerciseId() }) // Make sure exerciseId is correct - .subscribe({ - next: (response: FeedbackAnalysisResponse) => { - this.content.set(response.feedbackDetails); - this.distinctResultCount.set(response.distinctResultCount); // Store distinct result count - }, - error: (error) => { - this.alertService.error(error.message); - }, - }); + const response = await this.pagingService.search(state, { exerciseId: this.exerciseId() }); + this.content.set(response.feedbackDetails); + this.distinctResultCount.set(response.distinctResultCount); } setPage(newPage: number): void { 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 01b9a9afe150..ee9d08623dff 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,9 +1,11 @@ -import { HttpClient, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { PagingService } from 'app/exercises/shared/manage/paging.service'; import { SearchResult, SearchTermPageableSearch } from 'app/shared/table/pageable-table'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api-http.service'; + +export interface FeedbackAnalysisResponse { + feedbackDetails: SearchResult; + distinctResultCount: number; +} export interface FeedbackDetail { count: number; @@ -13,22 +15,13 @@ export interface FeedbackDetail { taskNumber: number; } -export interface FeedbackAnalysisResponse { - feedbackDetails: SearchResult; - distinctResultCount: number; -} - @Injectable({ providedIn: 'root' }) -export class FeedbackAnalysisService extends PagingService { - private resourceUrl = 'api'; - - constructor(private http: HttpClient) { +export class FeedbackAnalysisService extends BaseApiHttpService { + constructor() { super(); } - override search(pageable: SearchTermPageableSearch, options: { exerciseId: number }): Observable { - return this.http - .post(`${this.resourceUrl}/exercises/${options.exerciseId}/feedback-details-paged`, pageable, { observe: 'response' }) - .pipe(map((resp: HttpResponse) => resp.body!)); + search(pageable: SearchTermPageableSearch, options: { exerciseId: number }): Promise { + return this.post(`exercises/${options.exerciseId}/feedback-details-paged`, pageable); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java index b0b28eb32133..ee5c95af44c0 100644 --- a/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java @@ -31,12 +31,15 @@ import de.tum.cit.aet.artemis.assessment.domain.GradingCriterion; import de.tum.cit.aet.artemis.assessment.domain.GradingInstruction; import de.tum.cit.aet.artemis.assessment.domain.Result; +import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO; import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO; import de.tum.cit.aet.artemis.assessment.dto.ResultWithPointsPerGradingCriterionDTO; import de.tum.cit.aet.artemis.assessment.repository.FeedbackRepository; import de.tum.cit.aet.artemis.assessment.repository.GradingCriterionRepository; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.SortingOrder; +import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; import de.tum.cit.aet.artemis.exam.ExamUtilService; import de.tum.cit.aet.artemis.exam.domain.Exam; @@ -740,24 +743,33 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { feedback.setTestCase(testCase); participationUtilService.addFeedbackToResult(feedback, result); - List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); + SearchTermPageableSearchDTO searchDTO = new SearchTermPageableSearchDTO<>(); + searchDTO.setSortedColumn("detailText"); + searchDTO.setSortingOrder(SortingOrder.ASCENDING); + searchDTO.setPage(1); + searchDTO.setPageSize(10); - assertThat(response).isNotEmpty(); - FeedbackDetailDTO feedbackDetail = response.getFirst(); + FeedbackAnalysisResponseDTO response = request.postWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/feedback-details-paged", searchDTO, + FeedbackAnalysisResponseDTO.class, HttpStatus.OK); + + assertThat(response.feedbackDetails().getResultsOnPage()).isNotEmpty(); + FeedbackDetailDTO feedbackDetail = response.feedbackDetails().getResultsOnPage().get(0); assertThat(feedbackDetail.count()).isEqualTo(1); assertThat(feedbackDetail.relativeCount()).isEqualTo(100.0); assertThat(feedbackDetail.detailText()).isEqualTo("Some feedback"); assertThat(feedbackDetail.testCaseName()).isEqualTo("test1"); assertThat(feedbackDetail.taskNumber()).isEqualTo(1); + + assertThat(response.distinctResultCount()).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 participation1 = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); StudentParticipation participation2 = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student2"); - Result result = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation); + Result result1 = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation1); Result result2 = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation2); ProgrammingExerciseTestCase testCase = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "test1"); testCase.setId(1L); @@ -766,7 +778,7 @@ void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception feedback1.setPositive(false); feedback1.setDetailText("Some feedback"); feedback1.setTestCase(testCase); - participationUtilService.addFeedbackToResult(feedback1, result); + participationUtilService.addFeedbackToResult(feedback1, result1); Feedback feedback2 = new Feedback(); feedback2.setPositive(false); @@ -778,15 +790,23 @@ void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception feedback3.setPositive(false); feedback3.setDetailText("Some different feedback"); feedback3.setTestCase(testCase); - participationUtilService.addFeedbackToResult(feedback3, result); + participationUtilService.addFeedbackToResult(feedback3, result1); + + SearchTermPageableSearchDTO searchDTO = new SearchTermPageableSearchDTO<>(); + searchDTO.setSortedColumn("detailText"); + searchDTO.setSortingOrder(SortingOrder.ASCENDING); + searchDTO.setPage(1); + searchDTO.setPageSize(10); - List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); + FeedbackAnalysisResponseDTO response = request.postWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/feedback-details-paged", searchDTO, + FeedbackAnalysisResponseDTO.class, HttpStatus.OK); - assertThat(response).hasSize(2); + List feedbackDetails = response.feedbackDetails().getResultsOnPage(); + assertThat(feedbackDetails).hasSize(2); - FeedbackDetailDTO firstFeedbackDetail = response.stream().filter(feedbackDetail -> "Some feedback".equals(feedbackDetail.detailText())).findFirst().orElseThrow(); + FeedbackDetailDTO firstFeedbackDetail = feedbackDetails.stream().filter(feedbackDetail -> "Some feedback".equals(feedbackDetail.detailText())).findFirst().orElseThrow(); - FeedbackDetailDTO secondFeedbackDetail = response.stream().filter(feedbackDetail -> "Some different feedback".equals(feedbackDetail.detailText())).findFirst() + FeedbackDetailDTO secondFeedbackDetail = feedbackDetails.stream().filter(feedbackDetail -> "Some different feedback".equals(feedbackDetail.detailText())).findFirst() .orElseThrow(); assertThat(firstFeedbackDetail.count()).isEqualTo(2); @@ -800,15 +820,27 @@ void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception assertThat(secondFeedbackDetail.detailText()).isEqualTo("Some different feedback"); assertThat(secondFeedbackDetail.testCaseName()).isEqualTo("test1"); assertThat(secondFeedbackDetail.taskNumber()).isEqualTo(1); + + assertThat(response.distinctResultCount()).isEqualTo(2); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetAllFeedbackDetailsForExercise_NoParticipation() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); - List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); - assertThat(response).isEmpty(); + SearchTermPageableSearchDTO searchDTO = new SearchTermPageableSearchDTO<>(); + searchDTO.setSortedColumn("detailText"); + searchDTO.setSortingOrder(SortingOrder.ASCENDING); + searchDTO.setPage(1); + searchDTO.setPageSize(10); + + FeedbackAnalysisResponseDTO response = request.postWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/feedback-details-paged", searchDTO, + FeedbackAnalysisResponseDTO.class, HttpStatus.OK); + + assertThat(response.feedbackDetails().getResultsOnPage()).isEmpty(); + + assertThat(response.distinctResultCount()).isEqualTo(0); } } From bd0ff9300a1540da68dbbe474a734d0ed7fb4b72 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 13 Sep 2024 20:23:39 +0200 Subject: [PATCH 04/16] cleaned client side --- .../feedback-analysis.component.html | 8 ++--- .../feedback-analysis.component.ts | 29 ++++++++++--------- .../feedback-analysis.service.ts | 4 +-- 3 files changed, 19 insertions(+), 22 deletions(-) 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 e489766d0717..4e5c05e2efab 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 @@ -62,7 +62,7 @@

- + 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 50bb67c53b29..48e4192f8bd5 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,4 +1,4 @@ -import { Component, InputSignal, effect, inject, input, signal } from '@angular/core'; +import { Component, InputSignal, computed, effect, inject, input, signal } from '@angular/core'; import { FeedbackAnalysisService, FeedbackDetail } from './feedback-analysis.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { AlertService } from 'app/core/util/alert.service'; @@ -19,18 +19,16 @@ export class FeedbackAnalysisComponent { exerciseTitle: InputSignal = input.required(); exerciseId: InputSignal = input.required(); - // Signals for reactive state readonly page = signal(1); readonly pageSize = signal(15); - searchTerm = signal(''); // Initially empty + searchTerm = signal(''); readonly sortingOrder = signal(SortingOrder.DESCENDING); readonly sortedColumn = signal('count'); - readonly isLoading = signal(false); readonly content = signal>({ resultsOnPage: [], numberOfPages: 0 }); - distinctResultCount = signal(0); // To store the distinct result count + readonly totalItems = signal(0); + readonly collectionsSize = computed(() => this.content().numberOfPages * this.pageSize()); - // Inject dependencies private pagingService = inject(FeedbackAnalysisService); private alertService = inject(AlertService); private modalService = inject(NgbModal); @@ -40,10 +38,11 @@ export class FeedbackAnalysisComponent { readonly faSortDown = faSortDown; readonly faMagnifyingGlass = faMagnifyingGlass; readonly faMagnifyingGlassPlus = faMagnifyingGlassPlus; + readonly SortingOrder = SortingOrder; constructor() { effect(() => { - this.loadData(); // This will be triggered immediately upon page load + this.loadData(); }); } @@ -51,14 +50,18 @@ export class FeedbackAnalysisComponent { const state = { page: this.page(), pageSize: this.pageSize(), - searchTerm: this.searchTerm(), // Will be empty initially + searchTerm: this.searchTerm(), sortingOrder: this.sortingOrder(), sortedColumn: this.sortedColumn(), }; - const response = await this.pagingService.search(state, { exerciseId: this.exerciseId() }); - this.content.set(response.feedbackDetails); - this.distinctResultCount.set(response.distinctResultCount); + try { + const response = await this.pagingService.search(state, { exerciseId: this.exerciseId() }); + this.content.set(response.feedbackDetails); + this.totalItems.set(response.totalItems); + } catch (error) { + this.alertService.error('artemisApp.programmingExercise.configureGrading.feedbackAnalysis.error'); + } } setPage(newPage: number): void { @@ -77,7 +80,7 @@ export class FeedbackAnalysisComponent { } search(): void { - this.page.set(1); // Reset to page 1 when searching + this.page.set(1); this.loadData(); } @@ -85,6 +88,4 @@ export class FeedbackAnalysisComponent { const modalRef = this.modalService.open(FeedbackModalComponent, { centered: true }); modalRef.componentInstance.feedbackDetail = feedbackDetail; } - - protected readonly SortingOrder = SortingOrder; } 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 ee9d08623dff..1bc472908658 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 @@ -4,7 +4,7 @@ import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api- export interface FeedbackAnalysisResponse { feedbackDetails: SearchResult; - distinctResultCount: number; + totalItems: number; } export interface FeedbackDetail { @@ -21,7 +21,7 @@ export class FeedbackAnalysisService extends BaseApiHttpService { super(); } - search(pageable: SearchTermPageableSearch, options: { exerciseId: number }): Promise { + search(pageable: SearchTermPageableSearch, options: { exerciseId: number }): Promise { return this.post(`exercises/${options.exerciseId}/feedback-details-paged`, pageable); } } From 5a4800c5697d2d7277e059fe1e8d7cee967731a0 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 13 Sep 2024 20:48:48 +0200 Subject: [PATCH 05/16] cleaned client side and added new client tests --- .../feedback-analysis.component.ts | 4 +- .../feedback-analysis.component.spec.ts | 103 ++++++++++++++---- .../feedback-analysis.service.spec.ts | 32 +++--- 3 files changed, 101 insertions(+), 38 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 48e4192f8bd5..9277ed9c7976 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 @@ -29,7 +29,7 @@ export class FeedbackAnalysisComponent { readonly totalItems = signal(0); readonly collectionsSize = computed(() => this.content().numberOfPages * this.pageSize()); - private pagingService = inject(FeedbackAnalysisService); + private feedbackAnalysisService = inject(FeedbackAnalysisService); private alertService = inject(AlertService); private modalService = inject(NgbModal); @@ -56,7 +56,7 @@ export class FeedbackAnalysisComponent { }; try { - const response = await this.pagingService.search(state, { exerciseId: this.exerciseId() }); + const response = await this.feedbackAnalysisService.search(state, { exerciseId: this.exerciseId() }); this.content.set(response.feedbackDetails); this.totalItems.set(response.totalItems); } catch (error) { 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 0e9387b93e5b..234af2ab44e4 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 @@ -3,24 +3,30 @@ 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 { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { FeedbackAnalysisResponse, FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import '@angular/localize/init'; +import { signal } from '@angular/core'; describe('FeedbackAnalysisComponent', () => { let fixture: ComponentFixture; let component: FeedbackAnalysisComponent; let feedbackAnalysisService: FeedbackAnalysisService; - let getFeedbackDetailsSpy: jest.SpyInstance; + let searchSpy: jest.SpyInstance; const feedbackMock: FeedbackDetail[] = [ { 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 feedbackResponseMock: FeedbackAnalysisResponse = { + feedbackDetails: { resultsOnPage: feedbackMock, numberOfPages: 1 }, + totalItems: 2, + }; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ArtemisTestModule, TranslateModule.forRoot(), FeedbackAnalysisComponent], - declarations: [], providers: [ { provide: TranslateService, @@ -29,37 +35,96 @@ describe('FeedbackAnalysisComponent', () => { FeedbackAnalysisService, ], }).compileComponents(); + fixture = TestBed.createComponent(FeedbackAnalysisComponent); component = fixture.componentInstance; - component.exerciseId = 1; feedbackAnalysisService = fixture.debugElement.injector.get(FeedbackAnalysisService); - getFeedbackDetailsSpy = jest.spyOn(feedbackAnalysisService, 'getFeedbackDetailsForExercise').mockResolvedValue(feedbackMock); + searchSpy = jest.spyOn(feedbackAnalysisService, 'search').mockResolvedValue(feedbackResponseMock); + + (component.exerciseId as any) = signal(1); + (component.exerciseTitle as any) = signal('Sample Exercise Title'); + + fixture.detectChanges(); }); - describe('ngOnInit', () => { - it('should call loadFeedbackDetails when exerciseId is provided', async () => { - component.ngOnInit(); - await fixture.whenStable(); + afterEach(() => { + jest.restoreAllMocks(); + }); - expect(getFeedbackDetailsSpy).toHaveBeenCalledWith(1); - expect(component.feedbackDetails).toEqual(feedbackMock); + describe('on init', () => { + it('should load data on initialization', async () => { + await fixture.whenStable(); + expect(searchSpy).toHaveBeenCalled(); + expect(component.content().resultsOnPage).toEqual(feedbackMock); + expect(component.totalItems()).toBe(2); }); }); - describe('loadFeedbackDetails', () => { - it('should load feedback details and update the component state', async () => { - await component.loadFeedbackDetails(1); - expect(component.feedbackDetails).toEqual(feedbackMock); + describe('loadData', () => { + it('should load feedback details and update state correctly', async () => { + await component['loadData'](); + expect(searchSpy).toHaveBeenCalled(); + expect(component.content().resultsOnPage).toEqual(feedbackMock); + expect(component.totalItems()).toBe(2); }); it('should handle error while loading feedback details', async () => { - getFeedbackDetailsSpy.mockRejectedValue(new Error('Error loading feedback details')); + searchSpy.mockRejectedValueOnce(new Error('Error loading feedback details')); try { - await component.loadFeedbackDetails(1); + await component['loadData'](); } catch { - expect(component.feedbackDetails).toEqual([]); + expect(component.content().resultsOnPage).toEqual([]); + expect(component.totalItems()).toBe(0); } }); }); + + describe('setPage', () => { + it('should update page and reload data', async () => { + const loadDataSpy = jest.spyOn(component, 'loadData' as any); + + component.setPage(2); + expect(component.page()).toBe(2); + expect(loadDataSpy).toHaveBeenCalled(); + }); + }); + + describe('setSortedColumn', () => { + it('should update sortedColumn and sortingOrder, and reload data', async () => { + const loadDataSpy = jest.spyOn(component, 'loadData' as any); + + component.setSortedColumn('testCaseName'); + expect(component.sortedColumn()).toBe('testCaseName'); + expect(component.sortingOrder()).toBe('ASCENDING'); + expect(loadDataSpy).toHaveBeenCalled(); + + component.setSortedColumn('testCaseName'); + expect(component.sortingOrder()).toBe('DESCENDING'); + expect(loadDataSpy).toHaveBeenCalled(); + }); + }); + + describe('search', () => { + it('should reset page and load data when searching', async () => { + const loadDataSpy = jest.spyOn(component, 'loadData' as any); + + component.searchTerm.set('test'); + component.search(); + expect(component.page()).toBe(1); + expect(loadDataSpy).toHaveBeenCalled(); + }); + }); + + describe('openFeedbackModal', () => { + it('should open feedback modal with correct feedback detail', () => { + const modalService = fixture.debugElement.injector.get(NgbModal); + const modalSpy = jest.spyOn(modalService, 'open').mockReturnValue({ componentInstance: {} } as any); + + const feedbackDetail = feedbackMock[0]; + component.openFeedbackModal(feedbackDetail); + + expect(modalSpy).toHaveBeenCalled(); + }); + }); }); 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 a4f9a2a3ee42..52ff16cc5548 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,6 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { FeedbackAnalysisResponse, FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; describe('FeedbackAnalysisService', () => { let service: FeedbackAnalysisService; @@ -11,6 +11,11 @@ describe('FeedbackAnalysisService', () => { { detailText: 'Feedback 2', testCaseName: 'test2', count: 3, relativeCount: 15.0, taskNumber: 2 }, ]; + const feedbackResponseMock: FeedbackAnalysisResponse = { + feedbackDetails: { resultsOnPage: feedbackDetailsMock, numberOfPages: 1 }, + totalItems: 8, + }; + beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], @@ -25,26 +30,19 @@ describe('FeedbackAnalysisService', () => { httpMock.verify(); }); - describe('getFeedbackDetailsForExercise', () => { + describe('search', () => { 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); - }); + const pageable = { page: 1, pageSize: 10, searchTerm: '', sortingOrder: 'ASC', sortedColumn: 'count' }; + const exerciseId = 1; - it('should handle errors while retrieving feedback details', async () => { - const responsePromise = service.getFeedbackDetailsForExercise(1); + const responsePromise = service.search(pageable, { exerciseId }); - 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' }); + const req = httpMock.expectOne('api/exercises/1/feedback-details-paged'); + expect(req.request.method).toBe('POST'); + req.flush(feedbackResponseMock); - await expect(responsePromise).rejects.toThrow('Internal server error'); + const result = await responsePromise; + expect(result).toEqual(feedbackResponseMock); }); }); }); From 0d087aaefca0a3f7ed9f22625d066656535a4f1b Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 13 Sep 2024 21:08:24 +0200 Subject: [PATCH 06/16] changed server side naming --- .../artemis/assessment/dto/FeedbackAnalysisResponseDTO.java | 2 +- .../artemis/assessment/ResultServiceIntegrationTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java index 060af704bcb2..441b9830ce09 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java @@ -5,5 +5,5 @@ import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record FeedbackAnalysisResponseDTO(SearchResultPageDTO feedbackDetails, long distinctResultCount) { +public record FeedbackAnalysisResponseDTO(SearchResultPageDTO feedbackDetails, long totalItems) { } diff --git a/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java index ee5c95af44c0..781e9f18b32e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java @@ -760,7 +760,7 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { assertThat(feedbackDetail.testCaseName()).isEqualTo("test1"); assertThat(feedbackDetail.taskNumber()).isEqualTo(1); - assertThat(response.distinctResultCount()).isEqualTo(1); + assertThat(response.totalItems()).isEqualTo(1); } @Test @@ -821,7 +821,7 @@ void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception assertThat(secondFeedbackDetail.testCaseName()).isEqualTo("test1"); assertThat(secondFeedbackDetail.taskNumber()).isEqualTo(1); - assertThat(response.distinctResultCount()).isEqualTo(2); + assertThat(response.totalItems()).isEqualTo(2); } @Test @@ -840,7 +840,7 @@ void testGetAllFeedbackDetailsForExercise_NoParticipation() throws Exception { assertThat(response.feedbackDetails().getResultsOnPage()).isEmpty(); - assertThat(response.distinctResultCount()).isEqualTo(0); + assertThat(response.totalItems()).isEqualTo(0); } } From ec9fe4de54ee6bbbfcf7c94ebe47d2619a753dff Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Fri, 13 Sep 2024 21:14:52 +0200 Subject: [PATCH 07/16] fixed i18n files --- .../webapp/i18n/de/programmingExercise.json | 17 +++++++++++++++-- .../webapp/i18n/en/programmingExercise.json | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 6cfe36a77c3c..778a92e7ce7c 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -118,14 +118,22 @@ "workdir": "Verzeichnis", "allowOnlineEditor": { "title": "Online-Editor erlauben", - "alert": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" + "alert": "Es muss mindestens eine Option (Offline-IDE, Online-Editor oder Online-IDE) ausgewählt sein", + "alertNoTheia": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" }, "onlineEditor": "Online", "allowOfflineIde": { "title": "Offline-IDE erlauben", - "alert": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" + "alert": "Es muss mindestens eine Option (Offline-IDE, Online-Editor oder Online-IDE) ausgewählt sein", + "alertNoTheia": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" }, "offlineIde": "IDE", + "allowOnlineIde": { + "title": "Online-IDE erlauben", + "alert": "Es muss mindestens eine Option (Offline-IDE, Online-Editor oder Online-IDE) ausgewählt sein.", + "alertNoTheia": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" + }, + "onlineIde": "Online IDE", "showTestNamesToStudents": "Zeige die Test Namen den Studierenden", "showTestNamesToStudentsTooltip": "Durch Aktivierung dieser Option werden die Namen der automatischen Tests den Studierenden angezeigt. Lasse die Option deaktiviert, um keine visuelle Unterscheidung zwischen manuellem und automatischem Feedback für die Studierenden vorzunehmen.", "participationMode": "Teilnahmemodus", @@ -170,6 +178,11 @@ "projectType": "Projekttyp", "testRepositoryProjectType": "Projekttyp des Test-Repository", "packageName": "Package-Name", + "theiaImage": { + "title": "Konfiguration für Online IDE", + "noImageAvailable": "Die Online IDE ist für diese Programmiersprache noch nicht verfügbar.", + "alert": "Es muss eine gültige Konfiguration für die Online IDE ausgewählt werden." + }, "appName": "App-Name", "templateResult": "Ergebnis der Vorlage", "solutionResult": "Ergebnis der Musterlösung", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 7a3c8ff71bf7..fc28afe8a12a 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -130,14 +130,22 @@ "customizeDockerImage": "You can customize the Docker image. Make sure to provide it in amd64 and arm64 and include all build dependencies to guarantee a short build duration.", "allowOnlineEditor": { "title": "Allow Online Editor", - "alert": "At least one option (Offline IDE or Online Editor) must be selected" + "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected", + "alertNoTheia": "At least one option (Offline IDE or Online Editor) must be selected" }, "onlineEditor": "Online", "allowOfflineIde": { "title": "Allow Offline IDE", - "alert": "At least one option (Offline IDE or Online Editor) must be selected" + "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected", + "alertNoTheia": "At least one option (Offline IDE or Online Editor) must be selected" }, "offlineIde": "IDE", + "allowOnlineIde": { + "title": "Allow Online IDE", + "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected.", + "alertNoTheia": "At least one option (Offline IDE or Online Editor) must be selected" + }, + "onlineIde": "Online IDE", "showTestNamesToStudents": "Show Test Names to Students", "showTestNamesToStudentsTooltip": "Activate this option to show the names of the automated test cases to the students. Leave the option disabled to make no visual distinction between manual and automated feedback for the students.", "participationMode": "Participation Mode", @@ -172,6 +180,11 @@ "projectType": "Project Type", "testRepositoryProjectType": "Test Repository Project Type", "packageName": "Package Name", + "theiaImage": { + "title": "Configuration for Online IDE", + "noImageAvailable": "The Online IDE is not yet available for this programming language.", + "alert": "A valid configuration for the Online IDE must be selected." + }, "appName": "App Name", "templateResult": "Template Result", "solutionResult": "Solution Result", From 289d2eaec993b929a546cec1175f0f2437c33048 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sat, 14 Sep 2024 13:58:39 +0200 Subject: [PATCH 08/16] coderabbit and fixed client and server style --- .../assessment/service/ResultService.java | 35 +++++++++++-------- .../assessment/web/ResultResource.java | 2 +- .../Modal/feedback-modal.component.html | 7 +++- .../feedback-analysis.service.ts | 2 +- .../webapp/i18n/de/programmingExercise.json | 3 +- .../webapp/i18n/en/programmingExercise.json | 3 +- .../feedback-analysis.component.spec.ts | 1 - .../feedback-analysis.service.spec.ts | 9 ++++- 8 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java index a6ff3e57ab4d..0302b8947cb6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java @@ -560,37 +560,42 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Sea List feedbackDetails = studentParticipationRepository.findAggregatedFeedbackByExerciseId(exerciseId); String searchTerm = search.getSearchTerm() != null ? search.getSearchTerm().toLowerCase() : ""; - long totalFeedbackCount = feedbackDetails.size(); - - Predicate matchesSearchTerm = detail -> searchTerm.isEmpty() || detail.detailText().toLowerCase().contains(searchTerm) - || detail.testCaseName().toLowerCase().contains(searchTerm) || String.valueOf(detail.count()).contains(searchTerm) - || String.valueOf(detail.taskNumber()).contains(searchTerm) || String.valueOf(detail.relativeCount()).contains(searchTerm); - feedbackDetails = feedbackDetails.stream().map(detail -> { double relativeCount = (detail.count() * 100.0) / distinctResultCount; - int taskNumber = IntStream.range(0, tasks.size()) - .filter(i -> tasks.stream().toList().get(i).getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(detail.testCaseName()))).findFirst().orElse(-1) + 1; + int taskNumber = determineTaskNumberOfTestCase(detail.testCaseName(), tasks); return new FeedbackDetailDTO(detail.count(), relativeCount, detail.detailText(), detail.testCaseName(), taskNumber); - }).filter(matchesSearchTerm).collect(Collectors.toList()); + }).filter(matchesSearchTerm(searchTerm)).sorted(getComparatorForFeedbackDetails(search)).toList(); + + return paginateFeedbackDetails(feedbackDetails, search.getPage(), search.getPageSize()); + } + + private Predicate matchesSearchTerm(String searchTerm) { + return detail -> searchTerm.isEmpty() || detail.detailText().toLowerCase().contains(searchTerm) || detail.testCaseName().toLowerCase().contains(searchTerm) + || String.valueOf(detail.count()).contains(searchTerm) || String.valueOf(detail.taskNumber()).contains(searchTerm) + || String.valueOf(detail.relativeCount()).contains(searchTerm); + } + private Comparator getComparatorForFeedbackDetails(SearchTermPageableSearchDTO search) { Map> comparators = Map.of("count", Comparator.comparingLong(FeedbackDetailDTO::count), "detailText", Comparator.comparing(FeedbackDetailDTO::detailText, String.CASE_INSENSITIVE_ORDER), "testCaseName", Comparator.comparing(FeedbackDetailDTO::testCaseName, String.CASE_INSENSITIVE_ORDER), "taskNumber", Comparator.comparingInt(FeedbackDetailDTO::taskNumber), "relativeCount", Comparator.comparingDouble(FeedbackDetailDTO::relativeCount)); - Comparator comparator = comparators.getOrDefault(search.getSortedColumn(), (a, b) -> 0); - feedbackDetails.sort(search.getSortingOrder() == SortingOrder.ASCENDING ? comparator : comparator.reversed()); + return search.getSortingOrder() == SortingOrder.ASCENDING ? comparator : comparator.reversed(); + } - int pageSize = search.getPageSize(); - int page = search.getPage(); + private int determineTaskNumberOfTestCase(String testCaseName, Set tasks) { + return IntStream.range(0, tasks.size()).filter(i -> tasks.stream().toList().get(i).getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(testCaseName))) + .findFirst().orElse(-1) + 1; + } + private FeedbackAnalysisResponseDTO paginateFeedbackDetails(List feedbackDetails, int page, int pageSize) { int start = (page - 1) * pageSize; int end = Math.min(start + pageSize, feedbackDetails.size()); - List paginatedFeedbackDetails = feedbackDetails.subList(start, end); int totalPages = (feedbackDetails.size() + pageSize - 1) / pageSize; - return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(paginatedFeedbackDetails, totalPages), totalFeedbackCount); + return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(paginatedFeedbackDetails, totalPages), feedbackDetails.size()); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java index a69b3c583014..66badd3e24eb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java @@ -292,7 +292,7 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo * @param search The pageable search DTO containing page number, page size, sorting options, and a search term for filtering results. * @return A {@link ResponseEntity} containing a {@link FeedbackAnalysisResponseDTO}, which includes the paginated feedback details and the total count of distinct results. */ - @PostMapping("/exercises/{exerciseId}/feedback-details-paged") + @PostMapping("exercises/{exerciseId}/feedback-details-paged") @EnforceAtLeastEditorInExercise public ResponseEntity getFeedbackDetailsPaged(@PathVariable long exerciseId, @RequestBody SearchTermPageableSearchDTO search) { FeedbackAnalysisResponseDTO response = resultService.getFeedbackDetailsOnPage(exerciseId, search); diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html index 25ab8fdda6c4..eec7a8257dd9 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html @@ -25,5 +25,10 @@
- + 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 1bc472908658..780a3b2a7a0c 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 @@ -15,7 +15,7 @@ export interface FeedbackDetail { taskNumber: number; } -@Injectable({ providedIn: 'root' }) +@Injectable() export class FeedbackAnalysisService extends BaseApiHttpService { constructor() { super(); diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 778a92e7ce7c..3bb9dd85ea35 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -332,7 +332,8 @@ "error": "Beim Laden des Feedback ist ein Fehler aufgetreten.", "feedbackModal": { "header": "Fehler Details", - "feedbackTitle": "Testfall Feedback" + "feedbackTitle": "Testfall Feedback", + "ok": "Ok" } }, "help": { diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index fc28afe8a12a..7315277b5e6d 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -334,7 +334,8 @@ "error": "An error occurred while loading the feedback.", "feedbackModal": { "header": "Error Details", - "feedbackTitle": "Test Case Feedback" + "feedbackTitle": "Test Case Feedback", + "ok": "Ok" } }, "help": { 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 234af2ab44e4..072c9dfca4a9 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 @@ -35,7 +35,6 @@ describe('FeedbackAnalysisComponent', () => { FeedbackAnalysisService, ], }).compileComponents(); - fixture = TestBed.createComponent(FeedbackAnalysisComponent); component = fixture.componentInstance; feedbackAnalysisService = fixture.debugElement.injector.get(FeedbackAnalysisService); 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 52ff16cc5548..5784177baecc 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,6 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { FeedbackAnalysisResponse, FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { SearchTermPageableSearch, SortingOrder } from 'app/shared/table/pageable-table'; describe('FeedbackAnalysisService', () => { let service: FeedbackAnalysisService; @@ -32,7 +33,13 @@ describe('FeedbackAnalysisService', () => { describe('search', () => { it('should retrieve feedback details for a given exercise', async () => { - const pageable = { page: 1, pageSize: 10, searchTerm: '', sortingOrder: 'ASC', sortedColumn: 'count' }; + const pageable: SearchTermPageableSearch = { + page: 1, + pageSize: 10, + searchTerm: '', + sortingOrder: SortingOrder.ASCENDING, + sortedColumn: 'count', + }; const exerciseId = 1; const responsePromise = service.search(pageable, { exerciseId }); From c4b55034f8d3c498e23039a26299a2b62aa4e818 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 15 Sep 2024 14:37:31 +0200 Subject: [PATCH 09/16] Patrik feedback --- .../Modal/feedback-modal.component.html | 2 +- .../Modal/feedback-modal.component.ts | 4 ++-- .../feedback-analysis.component.html | 17 ++++++++++------- .../feedback-analysis.component.ts | 2 ++ .../feedback-analysis.service.ts | 4 ---- .../webapp/i18n/de/programmingExercise.json | 1 + .../webapp/i18n/en/programmingExercise.json | 1 + .../feedback-analysis.component.spec.ts | 17 ++++++++--------- 8 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html index eec7a8257dd9..c3e8ba00573a 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html @@ -29,6 +29,6 @@
diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts index 4a5d7807f4f9..b936ed90e066 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, inject } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; @@ -13,5 +13,5 @@ import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; export class FeedbackModalComponent { @Input() feedbackDetail!: FeedbackDetail; - constructor(public activeModal: NgbActiveModal) {} + activeModal: NgbActiveModal = inject(NgbActiveModal); } 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 4e5c05e2efab..3135de2d2092 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 @@ -2,11 +2,7 @@ @if (sortedColumn() === column) { - @if (sortingOrder() === SortingOrder.ASCENDING) { - - } @else { - - } + } @@ -15,7 +11,14 @@

- +
@@ -47,7 +50,7 @@

{{ item.count }} ({{ item.relativeCount | number: '1.0-0' }}%) - {{ item.detailText.length > 150 ? (item.detailText | slice: 0 : 100) + '...' : item.detailText }} + {{ item.detailText.length > maxFeedbackDetailTextLength ? (item.detailText | slice: 0 : 100) + '...' : item.detailText }} {{ item.taskNumber }} {{ item.testCaseName }} 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 9277ed9c7976..6dfb16de3735 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 @@ -39,6 +39,8 @@ export class FeedbackAnalysisComponent { readonly faMagnifyingGlass = faMagnifyingGlass; readonly faMagnifyingGlassPlus = faMagnifyingGlassPlus; readonly SortingOrder = SortingOrder; + readonly maxFeedbackDetailTextLength = 150; + readonly sortIcon = computed(() => (this.sortingOrder() === SortingOrder.ASCENDING ? this.faSortUp : this.faSortDown)); constructor() { effect(() => { 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 780a3b2a7a0c..57dbee819709 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 @@ -17,10 +17,6 @@ export interface FeedbackDetail { @Injectable() export class FeedbackAnalysisService extends BaseApiHttpService { - constructor() { - super(); - } - search(pageable: SearchTermPageableSearch, options: { exerciseId: number }): Promise { return this.post(`exercises/${options.exerciseId}/feedback-details-paged`, pageable); } diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 3bb9dd85ea35..cd08bacb787f 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -330,6 +330,7 @@ "errorCategory": "Fehlerkategorie", "totalItems": "Insgesamt {{count}} Elemente", "error": "Beim Laden des Feedback ist ein Fehler aufgetreten.", + "search": "Suche ...", "feedbackModal": { "header": "Fehler Details", "feedbackTitle": "Testfall Feedback", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 7315277b5e6d..4604344bee52 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -332,6 +332,7 @@ "errorCategory": "Error Category", "totalItems": "In total {{count}} items", "error": "An error occurred while loading the feedback.", + "search": "Search ...", "feedbackModal": { "header": "Error Details", "feedbackTitle": "Test Case Feedback", 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 072c9dfca4a9..afd1ffb3f466 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 @@ -6,7 +6,6 @@ import { FeedbackAnalysisComponent } from 'app/exercises/programming/manage/grad import { FeedbackAnalysisResponse, FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import '@angular/localize/init'; -import { signal } from '@angular/core'; describe('FeedbackAnalysisComponent', () => { let fixture: ComponentFixture; @@ -40,8 +39,8 @@ describe('FeedbackAnalysisComponent', () => { feedbackAnalysisService = fixture.debugElement.injector.get(FeedbackAnalysisService); searchSpy = jest.spyOn(feedbackAnalysisService, 'search').mockResolvedValue(feedbackResponseMock); - (component.exerciseId as any) = signal(1); - (component.exerciseTitle as any) = signal('Sample Exercise Title'); + fixture.componentRef.setInput('exerciseId', 1); + fixture.componentRef.setInput('exerciseTitle', 'Sample Exercise Title'); fixture.detectChanges(); }); @@ -53,7 +52,7 @@ describe('FeedbackAnalysisComponent', () => { describe('on init', () => { it('should load data on initialization', async () => { await fixture.whenStable(); - expect(searchSpy).toHaveBeenCalled(); + expect(searchSpy).toHaveBeenCalledOnce(); expect(component.content().resultsOnPage).toEqual(feedbackMock); expect(component.totalItems()).toBe(2); }); @@ -85,7 +84,7 @@ describe('FeedbackAnalysisComponent', () => { component.setPage(2); expect(component.page()).toBe(2); - expect(loadDataSpy).toHaveBeenCalled(); + expect(loadDataSpy).toHaveBeenCalledOnce(); }); }); @@ -96,11 +95,11 @@ describe('FeedbackAnalysisComponent', () => { component.setSortedColumn('testCaseName'); expect(component.sortedColumn()).toBe('testCaseName'); expect(component.sortingOrder()).toBe('ASCENDING'); - expect(loadDataSpy).toHaveBeenCalled(); + expect(loadDataSpy).toHaveBeenCalledOnce(); component.setSortedColumn('testCaseName'); expect(component.sortingOrder()).toBe('DESCENDING'); - expect(loadDataSpy).toHaveBeenCalled(); + expect(loadDataSpy).toHaveBeenCalledTimes(2); }); }); @@ -111,7 +110,7 @@ describe('FeedbackAnalysisComponent', () => { component.searchTerm.set('test'); component.search(); expect(component.page()).toBe(1); - expect(loadDataSpy).toHaveBeenCalled(); + expect(loadDataSpy).toHaveBeenCalledOnce(); }); }); @@ -123,7 +122,7 @@ describe('FeedbackAnalysisComponent', () => { const feedbackDetail = feedbackMock[0]; component.openFeedbackModal(feedbackDetail); - expect(modalSpy).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalledOnce(); }); }); }); From b6ad90eb1a91969dfffe673732a9fd0eeebfd947 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 16 Sep 2024 13:59:12 +0200 Subject: [PATCH 10/16] Patrik feedback --- .../feedback-analysis/Modal/feedback-modal.component.html | 8 ++++---- .../feedback-analysis/Modal/feedback-modal.component.ts | 4 ++-- .../feedback-analysis/feedback-analysis.component.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html index c3e8ba00573a..0c1e50b0bbce 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html @@ -5,15 +5,15 @@

-

{{ feedbackDetail.taskNumber }}

+

{{ feedbackDetail().taskNumber }}

-

{{ feedbackDetail.relativeCount }}% ({{ feedbackDetail.count }})

+

{{ feedbackDetail().relativeCount }}% ({{ feedbackDetail().count }})

-

{{ feedbackDetail.testCaseName }}

+

{{ feedbackDetail().testCaseName }}

@@ -22,7 +22,7 @@

-

{{ feedbackDetail.detailText }}

+

{{ feedbackDetail().detailText }}

+