From ef2de6533508f971bf2c3ac50b4fd1e331f19f7c Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Sun, 27 Oct 2024 21:46:57 +0100 Subject: [PATCH 01/15] =?UTF-8?q?error=20kategorien=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cit/aet/artemis/assessment/service/ResultService.java | 3 ++- .../exercise/repository/StudentParticipationRepository.java | 6 +++++- 2 files changed, 7 insertions(+), 2 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 a9e9050d5e2c..cea3b7ad6c61 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 @@ -603,7 +603,8 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee List processedDetails = feedbackDetailPage.getContent().stream().map(detail -> { String taskIndex = tasks.stream().filter(task -> task.getTaskName().equals(detail.taskNumber())).findFirst().map(task -> String.valueOf(tasks.indexOf(task) + 1)) .orElse("0"); - return new FeedbackDetailDTO(detail.count(), (detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), taskIndex, "StudentError"); + return new FeedbackDetailDTO(detail.count(), (detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), taskIndex, + detail.errorCategory()); }).toList(); // 8. Return the response DTO containing feedback details, total elements, and test case/task info diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java index 499818ace8a2..73a5a96e16f9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java @@ -1235,7 +1235,11 @@ SELECT COALESCE(AVG(p.presentationScore), 0) JOIN t.testCases tct WHERE t.exercise.id = :exerciseId AND tct.testName = f.testCase.testName ), ''), - '' + CASE + WHEN f.detailText LIKE 'ARES Security Error%' THEN 'Ares Error' + WHEN f.detailText LIKE 'Unwanted Statement found%' THEN 'AST Error' + ELSE 'Student Error' + END ) FROM StudentParticipation p JOIN p.results r ON r.id = ( From 715cb546b870843bd008eb7b1fab1225dd74f303 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 17:42:08 +0100 Subject: [PATCH 02/15] tasknumber replaced to taskname --- .../dto/FeedbackAnalysisResponseDTO.java | 3 +- .../assessment/dto/FeedbackDetailDTO.java | 2 +- .../assessment/service/ResultService.java | 37 +++++----- .../cit/aet/artemis/core/util/PageUtil.java | 9 ++- .../StudentParticipationRepository.java | 74 +++++++++---------- .../Modal/feedback-filter-modal.component.ts | 5 +- .../Modal/feedback-modal.component.html | 2 +- .../feedback-analysis.component.html | 6 +- .../feedback-analysis.component.ts | 8 +- .../feedback-analysis.service.ts | 4 +- .../feedback-analysis.component.spec.ts | 8 +- 11 files changed, 84 insertions(+), 74 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 d913f0c96e3f..6d8cfa811596 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 @@ -1,11 +1,12 @@ package de.tum.cit.aet.artemis.assessment.dto; import java.util.List; +import java.util.Set; 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 totalItems, int totalAmountOfTasks, List testCaseNames) { +public record FeedbackAnalysisResponseDTO(SearchResultPageDTO feedbackDetails, long totalItems, Set taskNames, List testCaseNames) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java index 7b3fd09ad57d..6d31e250f5d5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java @@ -3,5 +3,5 @@ import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record FeedbackDetailDTO(long count, double relativeCount, String detailText, String testCaseName, String taskNumber, String errorCategory) { +public record FeedbackDetailDTO(long count, double relativeCount, String detailText, String testCaseName, String taskName, String errorCategory) { } 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 cea3b7ad6c61..1d104c3a22de 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 @@ -576,15 +576,22 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId); // 2. Extract test case names using streams - List testCaseNames = programmingExercise.getTestCases().stream().map(ProgrammingExerciseTestCase::getTestName).toList(); + List activeTestCaseNames = programmingExercise.getTestCases().stream().filter(ProgrammingExerciseTestCase::isActive).map(ProgrammingExerciseTestCase::getTestName) + .toList(); List tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); + // Include "Not assigned to a task" if any feedback is without a task + Set taskNames = tasks.stream().map(ProgrammingExerciseTask::getTaskName).collect(Collectors.toSet()); + // 3. Generate filter task names directly - List filterTaskNames = data.getFilterTasks().stream().map(index -> { - int idx = Integer.parseInt(index); - return (idx > 0 && idx <= tasks.size()) ? tasks.get(idx - 1).getTaskName() : null; - }).filter(Objects::nonNull).toList(); + List includeUnassignedTasks = new ArrayList<>(taskNames); + if (!data.getFilterTasks().isEmpty()) { + includeUnassignedTasks.removeAll(data.getFilterTasks()); + } + else { + includeUnassignedTasks.clear(); + } // 4. Set minOccurrence and maxOccurrence based on filterOccurrence long minOccurrence = data.getFilterOccurrence().length == 2 ? Long.parseLong(data.getFilterOccurrence()[0]) : 0; @@ -595,21 +602,17 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee // 6. Fetch filtered feedback from the repository final Page feedbackDetailPage = studentParticipationRepository.findFilteredFeedbackByExerciseId(exerciseId, - StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), filterTaskNames, minOccurrence, maxOccurrence, - pageable); + StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), includeUnassignedTasks, minOccurrence, + maxOccurrence, pageable); // 7. Process feedback details // Map to index (+1 for 1-based indexing) - List processedDetails = feedbackDetailPage.getContent().stream().map(detail -> { - String taskIndex = tasks.stream().filter(task -> task.getTaskName().equals(detail.taskNumber())).findFirst().map(task -> String.valueOf(tasks.indexOf(task) + 1)) - .orElse("0"); - return new FeedbackDetailDTO(detail.count(), (detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), taskIndex, - detail.errorCategory()); - }).toList(); - - // 8. Return the response DTO containing feedback details, total elements, and test case/task info - return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(processedDetails, feedbackDetailPage.getTotalPages()), feedbackDetailPage.getTotalElements(), tasks.size(), - testCaseNames); + List processedDetails = feedbackDetailPage.getContent().stream().map(detail -> new FeedbackDetailDTO(detail.count(), + (detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), detail.taskName(), detail.errorCategory())).toList(); + + // 8. Return the response DTO containing feedback details, all task names (including "Not assigned to a task"), and test case names + return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(processedDetails, feedbackDetailPage.getTotalPages()), feedbackDetailPage.getTotalElements(), taskNames, + activeTestCaseNames); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java b/src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java index b1d3aaf5c20e..7417fe060e93 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java @@ -74,7 +74,14 @@ public enum ColumnMapping { FEEDBACK_ANALYSIS(Map.of( "count", "COUNT(f.id)", "detailText", "f.detailText", - "testCaseName", "f.testCase.testName" + "testCaseName", "f.testCase.testName", + "taskName", """ + COALESCE(( + SELECT MAX(t.taskName) + FROM ProgrammingExerciseTask t + JOIN t.testCases tct + WHERE t.exercise.id = :exerciseId AND tct.testName = f.testCase.testName + ), '')""" )); // @formatter:on diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java index 73a5a96e16f9..b0da09e45b6d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java @@ -1224,43 +1224,43 @@ SELECT COALESCE(AVG(p.presentationScore), 0) * @return A page of {@link FeedbackDetailDTO} objects representing the aggregated feedback details. */ @Query(""" - SELECT new de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO( - COUNT(f.id), - 0, - f.detailText, - f.testCase.testName, - COALESCE(( - SELECT t.taskName - FROM ProgrammingExerciseTask t - JOIN t.testCases tct - WHERE t.exercise.id = :exerciseId AND tct.testName = f.testCase.testName - ), ''), - CASE - WHEN f.detailText LIKE 'ARES Security Error%' THEN 'Ares Error' - WHEN f.detailText LIKE 'Unwanted Statement found%' THEN 'AST Error' - ELSE 'Student Error' - END - ) - FROM StudentParticipation p - JOIN p.results r ON r.id = ( - SELECT MAX(pr.id) - FROM p.results pr - WHERE pr.participation.id = p.id - ) - JOIN r.feedbacks f - WHERE p.exercise.id = :exerciseId - AND p.testRun = FALSE - AND f.positive = FALSE - AND (:searchTerm = '' OR LOWER(f.detailText) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(:searchTerm, '%', '\\%'), '_', '\\_'), '%')) ESCAPE '\\') - AND (:#{#filterTestCases != NULL && #filterTestCases.size() < 1} = TRUE OR f.testCase.testName IN (:filterTestCases)) - AND (:#{#filterTaskNames != NULL && #filterTaskNames.size() < 1} = TRUE OR f.testCase.testName IN ( - SELECT tct.testName - FROM ProgrammingExerciseTask t - JOIN t.testCases tct - WHERE t.taskName IN (:filterTaskNames) - )) - GROUP BY f.detailText, f.testCase.testName - HAVING COUNT(f.id) BETWEEN :minOccurrence AND :maxOccurrence + SELECT new de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO( + COUNT(f.id), + 0, + f.detailText, + f.testCase.testName, + COALESCE(( + SELECT MAX(t.taskName) + FROM ProgrammingExerciseTask t + JOIN t.testCases tct + WHERE t.exercise.id = :exerciseId AND tct.testName = f.testCase.testName + ), 'Not assigned to task'), + CASE + WHEN f.detailText LIKE 'ARES Security Error%' THEN 'Ares Error' + WHEN f.detailText LIKE 'Unwanted Statement found%' THEN 'AST Error' + ELSE 'Student Error' + END + ) + FROM StudentParticipation p + JOIN p.results r ON r.id = ( + SELECT MAX(pr.id) + FROM p.results pr + WHERE pr.participation.id = p.id + ) + JOIN r.feedbacks f + WHERE p.exercise.id = :exerciseId + AND p.testRun = FALSE + AND f.positive = FALSE + AND (:searchTerm = '' OR LOWER(f.detailText) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(:searchTerm, '%', '\\%'), '_', '\\_'), '%')) ESCAPE '\\') + AND (:#{#filterTestCases != NULL && #filterTestCases.size() < 1} = TRUE OR f.testCase.testName IN (:filterTestCases)) + AND (:#{#filterTaskNames != NULL && #filterTaskNames.size() < 1} = TRUE OR f.testCase.testName NOT IN ( + SELECT tct.testName + FROM ProgrammingExerciseTask t + JOIN t.testCases tct + WHERE t.taskName IN (:filterTaskNames) + )) + GROUP BY f.detailText, f.testCase.testName + HAVING COUNT(f.id) BETWEEN :minOccurrence AND :maxOccurrence """) Page findFilteredFeedbackByExerciseId(@Param("exerciseId") long exerciseId, @Param("searchTerm") String searchTerm, @Param("filterTestCases") List filterTestCases, @Param("filterTaskNames") List filterTaskNames, @Param("minOccurrence") long minOccurrence, diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts index 09e6784658b9..2f2e3141e4d8 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, inject, output, signal } from '@angular/core'; +import { Component, inject, output, signal } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { RangeSliderComponent } from 'app/shared/range-slider/range-slider.component'; import { FeedbackAnalysisService } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; @@ -28,11 +28,10 @@ export class FeedbackFilterModalComponent { readonly FILTER_TEST_CASES_KEY = 'feedbackAnalysis.testCases'; readonly FILTER_OCCURRENCE_KEY = 'feedbackAnalysis.occurrence'; - readonly totalAmountOfTasks = signal(0); readonly testCaseNames = signal([]); readonly minCount = signal(0); readonly maxCount = signal(0); - readonly taskArray = computed(() => Array.from({ length: this.totalAmountOfTasks() }, (_, i) => i + 1)); + readonly taskArray = signal([]); filters: FilterData = { tasks: [], 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 abf8a2bf5b47..8fcd1b1109c9 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 @@ -6,7 +6,7 @@

@@ -54,7 +54,7 @@

{{ item.taskNumber }} + {{ item.taskName }} {{ item.testCaseName }} {{ item.errorCategory }} 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 24855955f5b7..23e5818f09f9 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 @@ -50,7 +50,7 @@ export class FeedbackAnalysisComponent { readonly FILTER_TEST_CASES_KEY = 'feedbackAnalysis.testCases'; readonly FILTER_OCCURRENCE_KEY = 'feedbackAnalysis.occurrence'; readonly selectedFiltersCount = signal(0); - readonly totalAmountOfTasks = signal(0); + readonly taskNames = signal([]); readonly testCaseNames = signal([]); readonly minCount = signal(0); readonly maxCount = signal(0); @@ -89,7 +89,7 @@ export class FeedbackAnalysisComponent { }); this.content.set(response.feedbackDetails); this.totalItems.set(response.totalItems); - this.totalAmountOfTasks.set(response.totalAmountOfTasks); + this.taskNames.set(response.taskNames); this.testCaseNames.set(response.testCaseNames); } catch (error) { this.alertService.error('artemisApp.programmingExercise.configureGrading.feedbackAnalysis.error'); @@ -113,7 +113,7 @@ export class FeedbackAnalysisComponent { } isSortableColumn(column: string): boolean { - return ['count', 'detailText', 'testCaseName'].includes(column); + return ['count', 'detailText', 'testCaseName', 'taskName'].includes(column); } setSortedColumn(column: string): void { @@ -136,7 +136,7 @@ export class FeedbackAnalysisComponent { const modalRef = this.modalService.open(FeedbackFilterModalComponent, { centered: true, size: 'lg' }); modalRef.componentInstance.exerciseId = this.exerciseId; - modalRef.componentInstance.totalAmountOfTasks = this.totalAmountOfTasks; + modalRef.componentInstance.taskArray = this.taskNames; modalRef.componentInstance.testCaseNames = this.testCaseNames; modalRef.componentInstance.maxCount = this.maxCount; modalRef.componentInstance.filters = { 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 bb235de5a24c..1644b729c0e0 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 @@ -7,7 +7,7 @@ import { FilterData } from 'app/exercises/programming/manage/grading/feedback-an export interface FeedbackAnalysisResponse { feedbackDetails: SearchResult; totalItems: number; - totalAmountOfTasks: number; + taskNames: string[]; testCaseNames: string[]; } export interface FeedbackDetail { @@ -15,7 +15,7 @@ export interface FeedbackDetail { relativeCount: number; detailText: string; testCaseName: string; - taskNumber: string; + taskName: string; errorCategory: string; } @Injectable() 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 00ab084dd543..07c01fd0bf25 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 @@ -17,14 +17,14 @@ describe('FeedbackAnalysisComponent', () => { let localStorageService: LocalStorageService; const feedbackMock: FeedbackDetail[] = [ - { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 10, relativeCount: 50, taskNumber: '1', errorCategory: 'StudentError' }, - { detailText: 'Test feedback 2 detail', testCaseName: 'test2', count: 5, relativeCount: 25, taskNumber: '2', errorCategory: 'StudentError' }, + { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 10, relativeCount: 50, taskName: '1', errorCategory: 'StudentError' }, + { detailText: 'Test feedback 2 detail', testCaseName: 'test2', count: 5, relativeCount: 25, taskName: '2', errorCategory: 'StudentError' }, ]; const feedbackResponseMock: FeedbackAnalysisResponse = { feedbackDetails: { resultsOnPage: feedbackMock, numberOfPages: 1 }, totalItems: 2, - totalAmountOfTasks: 1, + taskNames: ['task1', 'task2'], testCaseNames: ['test1', 'test2'], }; @@ -165,7 +165,7 @@ describe('FeedbackAnalysisComponent', () => { testCases: ['testCase1'], occurrence: [component.minCount(), 5], }); - expect(modalInstance.totalAmountOfTasks).toBe(component.totalAmountOfTasks); + expect(modalInstance.taskNames).toBe(component.taskNames); expect(modalInstance.testCaseNames).toBe(component.testCaseNames); expect(modalInstance.exerciseId).toBe(component.exerciseId); expect(modalInstance.maxCount).toBe(component.maxCount); From f8099b6fcad8c5ee57d05d1e9ed1206598dbedb4 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 17:46:05 +0100 Subject: [PATCH 03/15] cleaned up --- .../assessment/service/ResultService.java | 42 +++++++++---------- .../StudentParticipationRepository.java | 11 +++-- 2 files changed, 27 insertions(+), 26 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 1d104c3a22de..763272561bd7 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 @@ -542,22 +542,22 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { } /** - * Retrieves paginated and filtered aggregated feedback details for a given exercise. + * Retrieves paginated and filtered aggregated feedback details for a given exercise, including the count of each unique feedback detail text, test case name, and task. *
* 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 numbers are assigned based on the associated test case names. A mapping between test cases and tasks is created using the set of tasks retrieved from the - * database. + * 1. The relative count is calculated as a percentage of the total distinct results for the exercise. + * 2. Task names are assigned based on associated test case names, with a mapping created between test cases and tasks from the exercise database. + * Feedback items not assigned to any task are labeled as "Not assigned to a task." *
- * Filtering: - * - **Search term**: Filters feedback details by the search term (case-insensitive). - * - **Test case names**: Filters feedback based on specific test case names (if provided). - * - **Task names**: Maps provided task numbers to task names and filters feedback based on the test cases associated with those tasks. - * - **Occurrences**: Filters feedback where the number of occurrences (COUNT) is between the provided minimum and maximum values (inclusive). + * It supports filtering by: + * - Search term: Case-insensitive filtering on feedback detail text. + * - Test case names: Filters feedback based on specific test case names. Only active test cases are included in the filtering options. + * - Task names: Filters feedback based on specified task names and includes unassigned tasks if "Not assigned to a task" is selected. + * - Occurrence range: Filters feedback where the number of occurrences (COUNT) is within the specified minimum and maximum range. *
* Pagination and sorting: * - 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. + * - The result is paginated according to the provided page number and page size. * * @param exerciseId The ID of the exercise for which feedback details should be retrieved. * @param data The {@link FeedbackPageableDTO} containing page number, page size, search term, sorting options, and filtering parameters (task names, test cases, @@ -565,8 +565,8 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { * @return A {@link FeedbackAnalysisResponseDTO} object containing: * - A {@link SearchResultPageDTO} of paginated feedback details. * - The total number of distinct results for the exercise. - * - The total number of tasks associated with the feedback. - * - A list of test case names included in the feedback. + * - A set of task names, including "Not assigned to a task" if applicable. + * - A list of active test case names used in the feedback. */ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, FeedbackPageableDTO data) { @@ -575,16 +575,15 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId); - // 2. Extract test case names using streams + // 2. Extract only active test case names List activeTestCaseNames = programmingExercise.getTestCases().stream().filter(ProgrammingExerciseTestCase::isActive).map(ProgrammingExerciseTestCase::getTestName) .toList(); + // 3. Retrieve all tasks and map their names List tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); - - // Include "Not assigned to a task" if any feedback is without a task Set taskNames = tasks.stream().map(ProgrammingExerciseTask::getTaskName).collect(Collectors.toSet()); - // 3. Generate filter task names directly + // Include unassigned tasks if specified by the filter; otherwise, only include specific tasks List includeUnassignedTasks = new ArrayList<>(taskNames); if (!data.getFilterTasks().isEmpty()) { includeUnassignedTasks.removeAll(data.getFilterTasks()); @@ -593,24 +592,23 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee includeUnassignedTasks.clear(); } - // 4. Set minOccurrence and maxOccurrence based on filterOccurrence + // 4. Set occurrence range based on filter parameters long minOccurrence = data.getFilterOccurrence().length == 2 ? Long.parseLong(data.getFilterOccurrence()[0]) : 0; long maxOccurrence = data.getFilterOccurrence().length == 2 ? Long.parseLong(data.getFilterOccurrence()[1]) : Integer.MAX_VALUE; - // 5. Create pageable object for pagination + // 5. Configure pagination and sorting final var pageable = PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.FEEDBACK_ANALYSIS); - // 6. Fetch filtered feedback from the repository + // 6. Fetch filtered feedback using the repository query final Page feedbackDetailPage = studentParticipationRepository.findFilteredFeedbackByExerciseId(exerciseId, StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), includeUnassignedTasks, minOccurrence, maxOccurrence, pageable); - // 7. Process feedback details - // Map to index (+1 for 1-based indexing) + // 7. Process and map feedback details, calculating relative count and assigning task names List processedDetails = feedbackDetailPage.getContent().stream().map(detail -> new FeedbackDetailDTO(detail.count(), (detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), detail.taskName(), detail.errorCategory())).toList(); - // 8. Return the response DTO containing feedback details, all task names (including "Not assigned to a task"), and test case names + // 8. Return response containing processed feedback details, task names, and active test case names return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(processedDetails, feedbackDetailPage.getTotalPages()), feedbackDetailPage.getTotalElements(), taskNames, activeTestCaseNames); } diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java index b0da09e45b6d..5279c09b5fc8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java @@ -1205,19 +1205,22 @@ SELECT COALESCE(AVG(p.presentationScore), 0) * - The number of occurrences of each feedback detail (COUNT). * - The relative count as a percentage of the total distinct results. * - The corresponding task name for each feedback item by checking if the feedback test case name is associated with a task. + * If a feedback item is not assigned to a task, it is labeled as "Not assigned to task." *
* It supports filtering by: * - Search term: Case-insensitive filtering on feedback detail text. - * - Test case names: Filters feedback based on specific test case names. - * - Task names: Filters feedback based on specific task names by mapping them to their associated test cases. - * - Occurrence range: Filters feedback based on the count of occurrences between the specified minimum and maximum values (inclusive). + * - Test case names: Filters feedback based on specific test case names (optional). + * - Task names: Filters feedback based on specific task names by mapping them to their associated test cases. If "Not assigned to task" + * is specified, only feedback items without an associated task will be included. + * - Occurrence range: Filters feedback where the number of occurrences (COUNT) is between the specified minimum and maximum values (inclusive). *
* Grouping is done by feedback detail text and test case name. The occurrence count is filtered using the HAVING clause. * * @param exerciseId The ID of the exercise for which feedback details should be retrieved. * @param searchTerm The search term used for filtering the feedback detail text (optional). - * @param filterTestCases List of test case names to filter the feedback results (optional). + * @param filterTestCases List of active test case names to filter the feedback results (optional). * @param filterTaskNames List of task names to filter feedback results based on the associated test cases (optional). + * If "Not assigned to task" is specified, only feedback entries without an associated task will be returned. * @param minOccurrence The minimum number of occurrences to include in the results. * @param maxOccurrence The maximum number of occurrences to include in the results. * @param pageable Pagination information to apply. From 4ce1930ecbe5da1603d2625f32c76ac1e51b9655 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 18:13:41 +0100 Subject: [PATCH 04/15] table col fixed --- .../feedback-analysis/feedback-analysis.component.html | 8 ++++++++ 1 file changed, 8 insertions(+) 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 982512eb1455..df4455c98358 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 @@ -25,6 +25,14 @@

+ + + + + + + + Date: Mon, 28 Oct 2024 18:55:13 +0100 Subject: [PATCH 05/15] empty table message added --- .../feedback-analysis.component.html | 35 +++++++++++++------ .../webapp/i18n/de/programmingExercise.json | 2 ++ .../webapp/i18n/en/programmingExercise.json | 2 ++ 3 files changed, 29 insertions(+), 10 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 df4455c98358..060c42358c25 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 @@ -6,6 +6,13 @@ } + + @if (this.selectedFiltersCount() > 0 || this.searchTerm() !== '') { +
+ } @else { +
+ } +

@@ -56,19 +63,27 @@

- @for (item of content().resultsOnPage; track item) { + @if (content().resultsOnPage === undefined || content().resultsOnPage.length === 0) { - {{ item.count }} ({{ item.relativeCount | number: '1.0-0' }}%) - - {{ item.detailText.length > MAX_FEEDBACK_DETAIL_TEXT_LENGTH ? (item.detailText | slice: 0 : 100) + '...' : item.detailText }} - - {{ item.taskName }} - {{ item.testCaseName }} - {{ item.errorCategory }} - - + + + } @else { + @for (item of content().resultsOnPage; track item) { + + {{ item.count }} ({{ item.relativeCount | number: '1.0-0' }}%) + + {{ item.detailText.length > MAX_FEEDBACK_DETAIL_TEXT_LENGTH ? (item.detailText | slice: 0 : 100) + '...' : item.detailText }} + + {{ item.taskName }} + {{ item.testCaseName }} + {{ item.errorCategory }} + + + + + } } diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index f26d5955a9ef..52443bc8d614 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -354,6 +354,8 @@ "error": "Beim Laden des Feedbacks ist ein Fehler aufgetreten.", "search": "Suche ...", "filter": "Filter", + "noData": "Es konnten keine Feedback Einträge für die Programmieraufgabe gefunden werden.", + "noDataFilter": "Für den spezifizierten Filter oder Suchbegriff konnten keine Feedback Einträge gefunden werden.", "feedbackModal": { "header": "Fehlerdetails", "feedbackTitle": "Feedback zu Testfällen", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 7659f48e78b2..00606c0f335b 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -354,6 +354,8 @@ "error": "An error occurred while loading the feedback.", "search": "Search ...", "filter": "Filters", + "noData": "No Feedback Entries could be found for the Programming Exercise.", + "noDataFilter": "No Feedback Entries could be found for the specified filter or search Term.", "feedbackModal": { "header": "Error Details", "feedbackTitle": "Test Case Feedback", From 49f0e7b9304c42b412ed44dfa2fd366d341724a0 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 20:41:02 +0100 Subject: [PATCH 06/15] sort icon added --- .../feedback-analysis.component.html | 10 +++-- .../feedback-analysis.component.ts | 11 +++++- .../app/shared/sort/sort-icon.component.html | 4 ++ .../app/shared/sort/sort-icon.component.scss | 4 ++ .../app/shared/sort/sort-icon.component.ts | 21 ++++++++++ .../util/shared/sort-icon.component.spec.ts | 38 +++++++++++++++++++ 6 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/main/webapp/app/shared/sort/sort-icon.component.html create mode 100644 src/main/webapp/app/shared/sort/sort-icon.component.scss create mode 100644 src/main/webapp/app/shared/sort/sort-icon.component.ts create mode 100644 src/test/javascript/spec/util/shared/sort-icon.component.spec.ts 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 060c42358c25..886025e7e46a 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,9 +1,11 @@ - - @if (sortedColumn() === column) { - - } +
+ + @if (isSortableColumn(column)) { + + } +
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 23e5818f09f9..2eaec645c26d 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 @@ -9,13 +9,14 @@ import { FeedbackModalComponent } from 'app/exercises/programming/manage/grading import { FeedbackFilterModalComponent, FilterData } from 'app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component'; import { LocalStorageService } from 'ngx-webstorage'; import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api-http.service'; +import { SortIconComponent } from 'app/shared/sort/sort-icon.component'; @Component({ selector: 'jhi-feedback-analysis', templateUrl: './feedback-analysis.component.html', styleUrls: ['./feedback-analysis.component.scss'], standalone: true, - imports: [ArtemisSharedCommonModule], + imports: [ArtemisSharedCommonModule, SortIconComponent], providers: [FeedbackAnalysisService], }) export class FeedbackAnalysisComponent { @@ -44,7 +45,6 @@ export class FeedbackAnalysisComponent { readonly faUpRightAndDownLeftFromCenter = faUpRightAndDownLeftFromCenter; readonly SortingOrder = SortingOrder; readonly MAX_FEEDBACK_DETAIL_TEXT_LENGTH = 150; - readonly sortIcon = computed(() => (this.sortingOrder() === SortingOrder.ASCENDING ? this.faSortUp : this.faSortDown)); readonly FILTER_TASKS_KEY = 'feedbackAnalysis.tasks'; readonly FILTER_TEST_CASES_KEY = 'feedbackAnalysis.testCases'; @@ -126,6 +126,13 @@ export class FeedbackAnalysisComponent { this.loadData(); } + getSortDirection(column: string): 'asc' | 'desc' | 'none' { + if (this.sortedColumn() === column) { + return this.sortingOrder() === SortingOrder.ASCENDING ? 'asc' : 'desc'; + } + return 'none'; + } + async openFilterModal(): Promise { const savedTasks = this.localStorage.retrieve(this.FILTER_TASKS_KEY); const savedTestCases = this.localStorage.retrieve(this.FILTER_TEST_CASES_KEY); diff --git a/src/main/webapp/app/shared/sort/sort-icon.component.html b/src/main/webapp/app/shared/sort/sort-icon.component.html new file mode 100644 index 000000000000..b88d6fdce772 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort-icon.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/src/main/webapp/app/shared/sort/sort-icon.component.scss b/src/main/webapp/app/shared/sort/sort-icon.component.scss new file mode 100644 index 000000000000..575055b8c2e8 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort-icon.component.scss @@ -0,0 +1,4 @@ +.sort-icon { + display: flex; + margin-left: 0.5rem; +} diff --git a/src/main/webapp/app/shared/sort/sort-icon.component.ts b/src/main/webapp/app/shared/sort/sort-icon.component.ts new file mode 100644 index 000000000000..001bf9683fd4 --- /dev/null +++ b/src/main/webapp/app/shared/sort/sort-icon.component.ts @@ -0,0 +1,21 @@ +import { Component, computed, input } from '@angular/core'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { CommonModule } from '@angular/common'; +import { faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; + +@Component({ + selector: 'app-sort-icon', + templateUrl: './sort-icon.component.html', + styleUrls: ['./sort-icon.component.scss'], + standalone: true, + imports: [FontAwesomeModule, CommonModule], +}) +export class SortIconComponent { + direction = input.required<'asc' | 'desc' | 'none'>(); + + faSortUp = faSortUp; + faSortDown = faSortDown; + + isAscending = computed(() => this.direction() === 'asc'); + isDescending = computed(() => this.direction() === 'desc'); +} diff --git a/src/test/javascript/spec/util/shared/sort-icon.component.spec.ts b/src/test/javascript/spec/util/shared/sort-icon.component.spec.ts new file mode 100644 index 000000000000..1703af52541a --- /dev/null +++ b/src/test/javascript/spec/util/shared/sort-icon.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { SortIconComponent } from 'app/shared/sort/sort-icon.component'; + +describe('SortIconComponent', () => { + let component: SortIconComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SortIconComponent, FontAwesomeModule], + }).compileComponents(); + + fixture = TestBed.createComponent(SortIconComponent); + component = fixture.componentInstance; + }); + + it('should set isAscending to true when direction is "asc"', () => { + fixture.componentRef.setInput('direction', 'asc'); + fixture.detectChanges(); + expect(component.isAscending()).toBeTrue(); + expect(component.isDescending()).toBeFalse(); + }); + + it('should set isDescending to true when direction is "desc"', () => { + fixture.componentRef.setInput('direction', 'desc'); + fixture.detectChanges(); + expect(component.isDescending()).toBeTrue(); + expect(component.isAscending()).toBeFalse(); + }); + + it('should set both isAscending and isDescending to false when direction is "none"', () => { + fixture.componentRef.setInput('direction', 'none'); + fixture.detectChanges(); + expect(component.isAscending()).toBeFalse(); + expect(component.isDescending()).toBeFalse(); + }); +}); From 9a45cf3447f36ecc5436f0aff28470233efc328e Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 20:46:05 +0100 Subject: [PATCH 07/15] constants updated --- .../grading/feedback-analysis/feedback-analysis.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 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 2eaec645c26d..3e2fe6adcf29 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 { private localStorage = inject(LocalStorageService); readonly page = signal(1); - readonly pageSize = signal(20); + readonly pageSize = signal(25); readonly searchTerm = signal(''); readonly sortingOrder = signal(SortingOrder.DESCENDING); readonly sortedColumn = signal('count'); @@ -44,7 +44,7 @@ export class FeedbackAnalysisComponent { readonly faFilter = faFilter; readonly faUpRightAndDownLeftFromCenter = faUpRightAndDownLeftFromCenter; readonly SortingOrder = SortingOrder; - readonly MAX_FEEDBACK_DETAIL_TEXT_LENGTH = 150; + readonly MAX_FEEDBACK_DETAIL_TEXT_LENGTH = 200; readonly FILTER_TASKS_KEY = 'feedbackAnalysis.tasks'; readonly FILTER_TEST_CASES_KEY = 'feedbackAnalysis.testCases'; From 2514fc06f20eef580408a99757c4fa987640e97d Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 21:58:52 +0100 Subject: [PATCH 08/15] errorcategory added to filter --- .../dto/FeedbackAnalysisResponseDTO.java | 3 +- .../assessment/dto/FeedbackPageableDTO.java | 10 ++ .../assessment/service/ResultService.java | 37 +++-- .../assessment/web/ResultResource.java | 65 ++++---- .../StudentParticipationRepository.java | 36 +++-- .../feedback-filter-modal.component.html | 20 +++ .../Modal/feedback-filter-modal.component.ts | 7 + .../feedback-analysis.component.ts | 12 ++ .../feedback-analysis.service.ts | 4 +- .../webapp/i18n/de/programmingExercise.json | 1 + .../webapp/i18n/en/programmingExercise.json | 1 + .../ResultServiceIntegrationTest.java | 8 +- .../feedback-analysis.component.spec.ts | 141 +++++++++++------- .../feedback-filter-modal.component.spec.ts | 23 ++- 14 files changed, 250 insertions(+), 118 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 6d8cfa811596..e56722f079cf 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 @@ -8,5 +8,6 @@ import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record FeedbackAnalysisResponseDTO(SearchResultPageDTO feedbackDetails, long totalItems, Set taskNames, List testCaseNames) { +public record FeedbackAnalysisResponseDTO(SearchResultPageDTO feedbackDetails, long totalItems, Set taskNames, List testCaseNames, + List errorCategories) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackPageableDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackPageableDTO.java index c63f9b5540f7..b65d09545fc2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackPageableDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackPageableDTO.java @@ -14,6 +14,8 @@ public class FeedbackPageableDTO extends PageableSearchDTO { private String searchTerm; + private List filterErrorCategories; + public List getFilterTasks() { return filterTasks; } @@ -45,4 +47,12 @@ public String getSearchTerm() { public void setSearchTerm(String searchTerm) { this.searchTerm = searchTerm; } + + public List getFilterErrorCategories() { + return filterErrorCategories; + } + + public void setFilterErrorCategories(List filterErrorCategories) { + this.filterErrorCategories = filterErrorCategories; + } } 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 763272561bd7..66577552150b 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 @@ -542,48 +542,53 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { } /** - * Retrieves paginated and filtered aggregated feedback details for a given exercise, including the count of each unique feedback detail text, test case name, and task. + * Retrieves paginated and filtered aggregated feedback details for a given exercise, including the count of each unique feedback detail text, + * test case name, task name, and error category. *
* For each feedback detail: * 1. The relative count is calculated as a percentage of the total distinct results for the exercise. * 2. Task names are assigned based on associated test case names, with a mapping created between test cases and tasks from the exercise database. * Feedback items not assigned to any task are labeled as "Not assigned to a task." + * 3. Error categories are classified as one of "Student Error," "Ares Error," or "AST Error," based on feedback content. *
* It supports filtering by: * - Search term: Case-insensitive filtering on feedback detail text. * - Test case names: Filters feedback based on specific test case names. Only active test cases are included in the filtering options. * - Task names: Filters feedback based on specified task names and includes unassigned tasks if "Not assigned to a task" is selected. * - Occurrence range: Filters feedback where the number of occurrences (COUNT) is within the specified minimum and maximum range. + * - Error categories: Filters feedback based on selected error categories, such as "Student Error," "Ares Error," and "AST Error." *
* Pagination and sorting: * - Sorting is applied based on the specified column and order (ascending or descending). * - The result is paginated according to the provided page number and page size. * * @param exerciseId The ID of the exercise for which feedback details should be retrieved. - * @param data The {@link FeedbackPageableDTO} containing page number, page size, search term, sorting options, and filtering parameters (task names, test cases, - * occurrence range). + * @param data The {@link FeedbackPageableDTO} containing page number, page size, search term, sorting options, and filtering parameters + * (task names, test cases, occurrence range, error categories). * @return A {@link FeedbackAnalysisResponseDTO} object containing: * - A {@link SearchResultPageDTO} of paginated feedback details. * - The total number of distinct results for the exercise. * - A set of task names, including "Not assigned to a task" if applicable. * - A list of active test case names used in the feedback. + * - A list of predefined error categories ("Student Error," "Ares Error," "AST Error") available for filtering. */ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, FeedbackPageableDTO data) { // 1. Fetch programming exercise with associated test cases ProgrammingExercise programmingExercise = programmingExerciseRepository.findWithTestCasesByIdElseThrow(exerciseId); + // 2. Get the distinct count of results for calculating relative feedback counts long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId); - // 2. Extract only active test case names + // 3. Extract only active test case names for use in filtering options List activeTestCaseNames = programmingExercise.getTestCases().stream().filter(ProgrammingExerciseTestCase::isActive).map(ProgrammingExerciseTestCase::getTestName) .toList(); - // 3. Retrieve all tasks and map their names + // 4. Retrieve all tasks associated with the exercise and map their names List tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); Set taskNames = tasks.stream().map(ProgrammingExerciseTask::getTaskName).collect(Collectors.toSet()); - // Include unassigned tasks if specified by the filter; otherwise, only include specific tasks + // 5. Include unassigned tasks if specified by the filter; otherwise, only include specified tasks List includeUnassignedTasks = new ArrayList<>(taskNames); if (!data.getFilterTasks().isEmpty()) { includeUnassignedTasks.removeAll(data.getFilterTasks()); @@ -592,25 +597,31 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee includeUnassignedTasks.clear(); } - // 4. Set occurrence range based on filter parameters + // 6. Define the occurrence range based on filter parameters long minOccurrence = data.getFilterOccurrence().length == 2 ? Long.parseLong(data.getFilterOccurrence()[0]) : 0; long maxOccurrence = data.getFilterOccurrence().length == 2 ? Long.parseLong(data.getFilterOccurrence()[1]) : Integer.MAX_VALUE; - // 5. Configure pagination and sorting + // 7. Define the error categories to filter based on user selection + List filterErrorCategories = data.getFilterErrorCategories(); + + // 8. Set up pagination and sorting based on input data final var pageable = PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.FEEDBACK_ANALYSIS); - // 6. Fetch filtered feedback using the repository query + // 9. Query the database to retrieve paginated and filtered feedback final Page feedbackDetailPage = studentParticipationRepository.findFilteredFeedbackByExerciseId(exerciseId, StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), includeUnassignedTasks, minOccurrence, - maxOccurrence, pageable); + maxOccurrence, filterErrorCategories, pageable); - // 7. Process and map feedback details, calculating relative count and assigning task names + // 10. Process and map feedback details, calculating relative count and assigning task names List processedDetails = feedbackDetailPage.getContent().stream().map(detail -> new FeedbackDetailDTO(detail.count(), (detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), detail.taskName(), detail.errorCategory())).toList(); - // 8. Return response containing processed feedback details, task names, and active test case names + // 11. Predefined error categories available for filtering on the client side + final List ERROR_CATEGORIES = List.of("Student Error", "Ares Error", "AST Error"); + + // 12. Return response containing processed feedback details, task names, active test case names, and error categories return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(processedDetails, feedbackDetailPage.getTotalPages()), feedbackDetailPage.getTotalElements(), taskNames, - activeTestCaseNames); + activeTestCaseNames, ERROR_CATEGORIES); } /** 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 5e28aa48b288..2a235145a04b 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 @@ -283,36 +283,49 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo } /** - * GET /exercises/{exerciseId}/feedback-details : 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 a search term and sorting by various fields. + * GET /exercises/{exerciseId}/feedback-details : Retrieves paginated and filtered aggregated feedback details for a specified exercise. *
- * Pagination is applied based on the provided query parameters, including page number, page size, sorting order, and search term. - * Sorting is applied by the specified sorted column and sorting order. If the provided sorted column is not valid for sorting (e.g., "taskNumber" or "errorCategory"), - * the sorting defaults to "count". + * This endpoint provides detailed feedback analytics, including: + * - The count and relative count (percentage) of each unique feedback entry. + * - Associated test case names. + * - Task names, mapped from test cases. *
- * Filtering is applied based on: - * - Task numbers (mapped to task names) - * - Test case names - * - Occurrence range (minimum and maximum occurrences) - *
- * The response contains both the paginated feedback details and the total count of distinct results for the exercise. + * Pagination, sorting, and filtering options allow flexible data retrieval: + *
    + *
  • Pagination: Based on page number and page size, as specified in the request.
  • + *
  • Sorting: By column (e.g., "count" or "detailText") and sorting order (ASCENDING or DESCENDING). + * If the specified column is not valid for sorting, the default sorting column is "count".
  • + *
  • Filtering: + *
      + *
    • Task names: Filters feedback entries by specific task names, including "Not assigned to task" if unassigned feedback is requested.
    • + *
    • Test case names: Filters feedback by specified test cases, using only active test cases from the exercise.
    • + *
    • Occurrence range: Filters by the minimum and maximum number of occurrences (inclusive).
    • + *
    • Search term: Case-insensitive filter applied to feedback detail text.
    • + *
    • Error categories: Filters feedback entries by specified error categories (e.g., "Student Error," "Ares Error," and "AST Error").
    • + *
    + *
  • + *
* - * @param exerciseId The ID of the exercise for which feedback details should be retrieved. - * @param data A {@link FeedbackPageableDTO} object containing pagination and filtering parameters, such as: - * - Page number - * - Page size - * - Search term (optional) - * - Sorting order (ASCENDING or DESCENDING) - * - Sorted column - * - Filter task numbers (optional) - * - Filter test case names (optional) - * - Occurrence range (optional) + * @param exerciseId The unique identifier of the exercise for which feedback details are requested. + * @param data A {@link FeedbackPageableDTO} object containing pagination, sorting, and filtering parameters, including: + *
    + *
  • Page number and page size
  • + *
  • Search term (optional)
  • + *
  • Sorting order (ASCENDING or DESCENDING)
  • + *
  • Sorted column
  • + *
  • Filter task names (optional)
  • + *
  • Filter test case names (optional)
  • + *
  • Occurrence range (optional)
  • + *
  • Error categories (optional)
  • + *
* @return A {@link ResponseEntity} containing a {@link FeedbackAnalysisResponseDTO}, which includes: - * - {@link SearchResultPageDTO < FeedbackDetailDTO >} feedbackDetails: Paginated feedback details for the exercise. - * - long totalItems: The total number of feedback items (used for pagination). - * - int totalAmountOfTasks: The total number of tasks associated with the feedback. - * - List testCaseNames: A list of test case names included in the feedback. + *
    + *
  • {@link SearchResultPageDTO < FeedbackDetailDTO >} feedbackDetails: Paginated and filtered feedback details for the exercise.
  • + *
  • long totalItems: The total count of feedback entries (for pagination).
  • + *
  • Set taskNames: A set of task names relevant to the feedback items, including "Not assigned to task" if applicable.
  • + *
  • List testCaseNames: A list of active test case names used in the feedback.
  • + *
  • List errorCategories: The list of error categories included in the feedback details, such as "Student Error," "Ares Error," and "AST Error".
  • + *
*/ @GetMapping("exercises/{exerciseId}/feedback-details") @EnforceAtLeastInstructorInExercise diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java index 5279c09b5fc8..3681b3a94221 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java @@ -1199,31 +1199,36 @@ SELECT COALESCE(AVG(p.presentationScore), 0) double getAvgPresentationScoreByCourseId(@Param("courseId") long courseId); /** - * Retrieves aggregated feedback details for a given exercise, including the count of each unique feedback detail text, test case name, and task. + * Retrieves aggregated feedback details for a given exercise, including the count of each unique feedback detail text, + * test case name, task, and error category. *
* The query calculates: * - The number of occurrences of each feedback detail (COUNT). * - The relative count as a percentage of the total distinct results. * - The corresponding task name for each feedback item by checking if the feedback test case name is associated with a task. * If a feedback item is not assigned to a task, it is labeled as "Not assigned to task." + * - The error category for each feedback item, classified as one of "Student Error", "Ares Error", or "AST Error". *
* It supports filtering by: * - Search term: Case-insensitive filtering on feedback detail text. * - Test case names: Filters feedback based on specific test case names (optional). - * - Task names: Filters feedback based on specific task names by mapping them to their associated test cases. If "Not assigned to task" - * is specified, only feedback items without an associated task will be included. + * - Task names: Filters feedback based on specific task names by mapping them to their associated test cases. + * If "Not assigned to task" is specified, only feedback items without an associated task will be included. * - Occurrence range: Filters feedback where the number of occurrences (COUNT) is between the specified minimum and maximum values (inclusive). + * - Error categories: Filters feedback based on error categories, which can be "Student Error", "Ares Error", or "AST Error". *
- * Grouping is done by feedback detail text and test case name. The occurrence count is filtered using the HAVING clause. + * Grouping is done by feedback detail text, test case name, and error category. The occurrence count is filtered using the HAVING clause. * - * @param exerciseId The ID of the exercise for which feedback details should be retrieved. - * @param searchTerm The search term used for filtering the feedback detail text (optional). - * @param filterTestCases List of active test case names to filter the feedback results (optional). - * @param filterTaskNames List of task names to filter feedback results based on the associated test cases (optional). - * If "Not assigned to task" is specified, only feedback entries without an associated task will be returned. - * @param minOccurrence The minimum number of occurrences to include in the results. - * @param maxOccurrence The maximum number of occurrences to include in the results. - * @param pageable Pagination information to apply. + * @param exerciseId The ID of the exercise for which feedback details should be retrieved. + * @param searchTerm The search term used for filtering the feedback detail text (optional). + * @param filterTestCases List of active test case names to filter the feedback results (optional). + * @param filterTaskNames List of task names to filter feedback results based on the associated test cases (optional). + * If "Not assigned to task" is specified, only feedback entries without an associated task will be returned. + * @param minOccurrence The minimum number of occurrences to include in the results. + * @param maxOccurrence The maximum number of occurrences to include in the results. + * @param filterErrorCategories List of error categories to filter the feedback results. Supported categories include "Student Error", + * "Ares Error", and "AST Error". + * @param pageable Pagination information to apply. * @return A page of {@link FeedbackDetailDTO} objects representing the aggregated feedback details. */ @Query(""" @@ -1262,12 +1267,17 @@ SELECT MAX(pr.id) JOIN t.testCases tct WHERE t.taskName IN (:filterTaskNames) )) + AND (:#{#filterErrorCategories != NULL && #filterErrorCategories.size() < 1} = TRUE OR CASE + WHEN f.detailText LIKE 'ARES Security Error%' THEN 'Ares Error' + WHEN f.detailText LIKE 'Unwanted Statement found%' THEN 'AST Error' + ELSE 'Student Error' + END IN (:filterErrorCategories)) GROUP BY f.detailText, f.testCase.testName HAVING COUNT(f.id) BETWEEN :minOccurrence AND :maxOccurrence """) Page findFilteredFeedbackByExerciseId(@Param("exerciseId") long exerciseId, @Param("searchTerm") String searchTerm, @Param("filterTestCases") List filterTestCases, @Param("filterTaskNames") List filterTaskNames, @Param("minOccurrence") long minOccurrence, - @Param("maxOccurrence") long maxOccurrence, Pageable pageable); + @Param("maxOccurrence") long maxOccurrence, @Param("filterErrorCategories") List filterErrorCategories, Pageable pageable); /** * Counts the distinct number of latest results for a given exercise, excluding those in practice mode. diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html index 0571f1039ca6..b3c8d219d314 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html @@ -43,6 +43,26 @@

diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts index 2f2e3141e4d8..7ab6bd754de4 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts @@ -9,6 +9,7 @@ export interface FilterData { tasks: string[]; testCases: string[]; occurrence: number[]; + errorCategories: string[]; } @Component({ @@ -27,22 +28,26 @@ export class FeedbackFilterModalComponent { readonly FILTER_TASKS_KEY = 'feedbackAnalysis.tasks'; readonly FILTER_TEST_CASES_KEY = 'feedbackAnalysis.testCases'; readonly FILTER_OCCURRENCE_KEY = 'feedbackAnalysis.occurrence'; + readonly FILTER_ERROR_CATEGORIES_KEY = 'feedbackAnalysis.errorCategories'; readonly testCaseNames = signal([]); readonly minCount = signal(0); readonly maxCount = signal(0); readonly taskArray = signal([]); + readonly errorCategories = signal([]); filters: FilterData = { tasks: [], testCases: [], occurrence: [this.minCount(), this.maxCount() || 1], + errorCategories: [], }; applyFilter(): void { this.localStorage.store(this.FILTER_TASKS_KEY, this.filters.tasks); this.localStorage.store(this.FILTER_TEST_CASES_KEY, this.filters.testCases); this.localStorage.store(this.FILTER_OCCURRENCE_KEY, this.filters.occurrence); + this.localStorage.store(this.FILTER_ERROR_CATEGORIES_KEY, this.filters.errorCategories); this.filterApplied.emit(this.filters); this.activeModal.close(); } @@ -51,10 +56,12 @@ export class FeedbackFilterModalComponent { this.localStorage.clear(this.FILTER_TASKS_KEY); this.localStorage.clear(this.FILTER_TEST_CASES_KEY); this.localStorage.clear(this.FILTER_OCCURRENCE_KEY); + this.localStorage.clear(this.FILTER_ERROR_CATEGORIES_KEY); this.filters = { tasks: [], testCases: [], occurrence: [this.minCount(), this.maxCount()], + errorCategories: [], }; this.filterApplied.emit(this.filters); this.activeModal.close(); 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 3e2fe6adcf29..fe4ff839b4a0 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 @@ -49,11 +49,13 @@ export class FeedbackAnalysisComponent { readonly FILTER_TASKS_KEY = 'feedbackAnalysis.tasks'; readonly FILTER_TEST_CASES_KEY = 'feedbackAnalysis.testCases'; readonly FILTER_OCCURRENCE_KEY = 'feedbackAnalysis.occurrence'; + readonly FILTER_ERROR_CATEGORIES_KEY = 'feedbackAnalysis.errorCategories'; readonly selectedFiltersCount = signal(0); readonly taskNames = signal([]); readonly testCaseNames = signal([]); readonly minCount = signal(0); readonly maxCount = signal(0); + readonly errorCategories = signal([]); private readonly debounceLoadData = BaseApiHttpService.debounce(this.loadData.bind(this), 300); @@ -69,6 +71,7 @@ export class FeedbackAnalysisComponent { const savedTasks = this.localStorage.retrieve(this.FILTER_TASKS_KEY) || []; const savedTestCases = this.localStorage.retrieve(this.FILTER_TEST_CASES_KEY) || []; const savedOccurrence = this.localStorage.retrieve(this.FILTER_OCCURRENCE_KEY) || []; + const savedErrorCategories = this.localStorage.retrieve(this.FILTER_ERROR_CATEGORIES_KEY) || []; const state = { page: this.page(), @@ -76,6 +79,7 @@ export class FeedbackAnalysisComponent { searchTerm: this.searchTerm() || '', sortingOrder: this.sortingOrder(), sortedColumn: this.sortedColumn(), + filterErrorCategories: this.errorCategories(), }; try { @@ -85,12 +89,14 @@ export class FeedbackAnalysisComponent { tasks: this.selectedFiltersCount() !== 0 ? savedTasks : [], testCases: this.selectedFiltersCount() !== 0 ? savedTestCases : [], occurrence: this.selectedFiltersCount() !== 0 ? savedOccurrence : [], + errorCategories: this.selectedFiltersCount() !== 0 ? savedErrorCategories : [], }, }); this.content.set(response.feedbackDetails); this.totalItems.set(response.totalItems); this.taskNames.set(response.taskNames); this.testCaseNames.set(response.testCaseNames); + this.errorCategories.set(response.errorCategories); } catch (error) { this.alertService.error('artemisApp.programmingExercise.configureGrading.feedbackAnalysis.error'); } @@ -137,6 +143,7 @@ export class FeedbackAnalysisComponent { const savedTasks = this.localStorage.retrieve(this.FILTER_TASKS_KEY); const savedTestCases = this.localStorage.retrieve(this.FILTER_TEST_CASES_KEY); const savedOccurrence = this.localStorage.retrieve(this.FILTER_OCCURRENCE_KEY); + const savedErrorCategories = this.localStorage.retrieve(this.FILTER_ERROR_CATEGORIES_KEY); this.minCount.set(0); this.maxCount.set(await this.feedbackAnalysisService.getMaxCount(this.exerciseId())); @@ -146,10 +153,12 @@ export class FeedbackAnalysisComponent { modalRef.componentInstance.taskArray = this.taskNames; modalRef.componentInstance.testCaseNames = this.testCaseNames; modalRef.componentInstance.maxCount = this.maxCount; + modalRef.componentInstance.errorCategories = this.errorCategories; modalRef.componentInstance.filters = { tasks: this.selectedFiltersCount() !== 0 ? savedTasks : [], testCases: this.selectedFiltersCount() !== 0 ? savedTestCases : [], occurrence: this.selectedFiltersCount() !== 0 ? savedOccurrence : [this.minCount(), this.maxCount()], + errorCategories: this.selectedFiltersCount() !== 0 ? savedErrorCategories : [], }; modalRef.componentInstance.filterApplied.subscribe((filters: any) => { this.applyFilters(filters); @@ -169,6 +178,9 @@ export class FeedbackAnalysisComponent { if (filters.testCases && filters.testCases.length > 0) { count += filters.testCases.length; } + if (filters.errorCategories && filters.errorCategories.length > 0) { + count += filters.errorCategories.length; + } if (filters.occurrence && (filters.occurrence[0] !== 0 || filters.occurrence[1] !== this.maxCount())) { count++; } 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 1644b729c0e0..4228e50c329a 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 @@ -9,6 +9,7 @@ export interface FeedbackAnalysisResponse { totalItems: number; taskNames: string[]; testCaseNames: string[]; + errorCategories: string[]; } export interface FeedbackDetail { count: number; @@ -29,7 +30,8 @@ export class FeedbackAnalysisService extends BaseApiHttpService { .set('sortedColumn', pageable.sortedColumn) .set('filterTasks', options.filters.tasks.join(',')) .set('filterTestCases', options.filters.testCases.join(',')) - .set('filterOccurrence', options.filters.occurrence.join(',')); + .set('filterOccurrence', options.filters.occurrence.join(',')) + .set('filterErrorCategories', options.filters.errorCategories.join(',')); return this.get(`exercises/${options.exerciseId}/feedback-details`, { params }); } diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 52443bc8d614..69e78eee358b 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -365,6 +365,7 @@ "modalTitle": "Filteroptionen", "task": "Tasks", "testcase": "Testfälle", + "errorCategory": "Fehlerkategorien", "occurrence": "Häufigkeit", "clear": "Filter zurücksetzen", "cancel": "Abbrechen", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 00606c0f335b..a6ddfd930554 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -365,6 +365,7 @@ "modalTitle": "Filter Options", "task": "Tasks", "testcase": "Test Cases", + "errorCategory": "Error Categories", "occurrence": "Occurrence", "clear": "Clear Filter", "cancel": "Cancel", 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 1d04c504db90..2413dcab2078 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 @@ -739,13 +739,11 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { feedback.setTestCase(testCase); StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); - Result result = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation); - participationUtilService.addFeedbackToResult(feedback, result); String url = "/api/exercises/" + programmingExercise.getId() + "/feedback-details" + "?page=1&pageSize=10&sortedColumn=detailText&sortingOrder=ASCENDING" - + "&searchTerm=&filterTasks=&filterTestCases=&filterOccurrence="; + + "&searchTerm=&filterTasks=&filterTestCases=&filterOccurrence=&filterErrorCategories="; FeedbackAnalysisResponseDTO response = request.get(url, HttpStatus.OK, FeedbackAnalysisResponseDTO.class); @@ -755,6 +753,7 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { assertThat(feedbackDetail.relativeCount()).isEqualTo(100.0); assertThat(feedbackDetail.detailText()).isEqualTo("Some feedback"); assertThat(feedbackDetail.testCaseName()).isEqualTo("test1"); + assertThat(response.errorCategories()).containsExactlyInAnyOrder("Student Error", "Ares Error", "AST Error"); assertThat(response.totalItems()).isEqualTo(1); } @@ -789,7 +788,7 @@ void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception participationUtilService.addFeedbackToResult(feedback3, result1); String url = "/api/exercises/" + programmingExercise.getId() + "/feedback-details" + "?page=1&pageSize=10&sortedColumn=detailText&sortingOrder=ASCENDING" - + "&searchTerm=&filterTasks=&filterTestCases=&filterOccurrence="; + + "&searchTerm=&filterTasks=&filterTestCases=&filterOccurrence=&filterErrorCategories="; FeedbackAnalysisResponseDTO response = request.get(url, HttpStatus.OK, FeedbackAnalysisResponseDTO.class); @@ -810,6 +809,7 @@ void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception assertThat(secondFeedbackDetail.relativeCount()).isEqualTo(50.0); assertThat(secondFeedbackDetail.detailText()).isEqualTo("Some different feedback"); assertThat(secondFeedbackDetail.testCaseName()).isEqualTo("test1"); + assertThat(response.errorCategories()).containsExactlyInAnyOrder("Student Error", "Ares Error", "AST Error"); assertThat(response.totalItems()).isEqualTo(2); } 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 07c01fd0bf25..37e7821fe502 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 @@ -17,8 +17,22 @@ describe('FeedbackAnalysisComponent', () => { let localStorageService: LocalStorageService; const feedbackMock: FeedbackDetail[] = [ - { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 10, relativeCount: 50, taskName: '1', errorCategory: 'StudentError' }, - { detailText: 'Test feedback 2 detail', testCaseName: 'test2', count: 5, relativeCount: 25, taskName: '2', errorCategory: 'StudentError' }, + { + detailText: 'Test feedback 1 detail', + testCaseName: 'test1', + count: 10, + relativeCount: 50, + taskName: '1', + errorCategory: 'Student Error', + }, + { + detailText: 'Test feedback 2 detail', + testCaseName: 'test2', + count: 5, + relativeCount: 25, + taskName: '2', + errorCategory: 'AST Error', + }, ]; const feedbackResponseMock: FeedbackAnalysisResponse = { @@ -26,6 +40,7 @@ describe('FeedbackAnalysisComponent', () => { totalItems: 2, taskNames: ['task1', 'task2'], testCaseNames: ['test1', 'test2'], + errorCategories: ['Student Error', 'AST Error', 'Ares Error'], }; beforeEach(async () => { @@ -66,6 +81,7 @@ describe('FeedbackAnalysisComponent', () => { expect(searchSpy).toHaveBeenCalledOnce(); expect(component.content().resultsOnPage).toEqual(feedbackMock); expect(component.totalItems()).toBe(2); + expect(component.errorCategories()).toEqual(feedbackResponseMock.errorCategories); }); }); @@ -75,6 +91,7 @@ describe('FeedbackAnalysisComponent', () => { expect(searchSpy).toHaveBeenCalledTimes(2); expect(component.content().resultsOnPage).toEqual(feedbackMock); expect(component.totalItems()).toBe(2); + expect(component.errorCategories()).toEqual(feedbackResponseMock.errorCategories); }); it('should handle error while loading feedback details', async () => { @@ -89,59 +106,6 @@ describe('FeedbackAnalysisComponent', () => { }); }); - 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).toHaveBeenCalledOnce(); - }); - }); - - 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).toHaveBeenCalledOnce(); - - component.setSortedColumn('testCaseName'); - expect(component.sortingOrder()).toBe('DESCENDING'); - expect(loadDataSpy).toHaveBeenCalledTimes(2); - }); - }); - - describe('search', () => { - beforeEach(() => { - jest.spyOn(component, 'debounceLoadData' as any).mockImplementation(() => { - component['loadData'](); - }); - }); - - it('should reset page and load data when searching', async () => { - const loadDataSpy = jest.spyOn(component, 'loadData' as any); - component.searchTerm.set('test'); - await component.search(component.searchTerm()); - expect(component.page()).toBe(1); - expect(loadDataSpy).toHaveBeenCalledOnce(); - }); - }); - - 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).toHaveBeenCalledOnce(); - }); - }); - describe('openFilterModal', () => { it('should open filter modal and pass correct form values and properties', async () => { const modalService = fixture.debugElement.injector.get(NgbModal); @@ -151,7 +115,11 @@ describe('FeedbackAnalysisComponent', () => { }, } as any); const getMaxCountSpy = jest.spyOn(feedbackAnalysisService, 'getMaxCount').mockResolvedValue(10); - jest.spyOn(localStorageService, 'retrieve').mockReturnValueOnce(['task1']).mockReturnValueOnce(['testCase1']).mockReturnValueOnce([component.minCount(), 5]); + jest.spyOn(localStorageService, 'retrieve') + .mockReturnValueOnce(['task1']) + .mockReturnValueOnce(['testCase1']) + .mockReturnValueOnce([component.minCount(), 5]) + .mockReturnValueOnce(['Student Error']); component.maxCount.set(5); component.selectedFiltersCount.set(1); @@ -164,8 +132,9 @@ describe('FeedbackAnalysisComponent', () => { tasks: ['task1'], testCases: ['testCase1'], occurrence: [component.minCount(), 5], + errorCategories: ['Student Error'], }); - expect(modalInstance.taskNames).toBe(component.taskNames); + expect(modalInstance.taskArray).toBe(component.taskNames); expect(modalInstance.testCaseNames).toBe(component.testCaseNames); expect(modalInstance.exerciseId).toBe(component.exerciseId); expect(modalInstance.maxCount).toBe(component.maxCount); @@ -181,6 +150,7 @@ describe('FeedbackAnalysisComponent', () => { tasks: ['task1'], testCases: ['testCase1'], occurrence: [component.minCount(), 10], + errorCategories: ['Student Error'], }; component.applyFilters(filters); @@ -197,10 +167,11 @@ describe('FeedbackAnalysisComponent', () => { tasks: ['task1', 'task2'], testCases: ['testCase1'], occurrence: [component.minCount(), component.maxCount()], + errorCategories: ['AST Error'], }; const count = component.countAppliedFilters(filters); - expect(count).toBe(3); + expect(count).toBe(4); }); it('should return 0 if no filters are applied', () => { @@ -208,9 +179,63 @@ describe('FeedbackAnalysisComponent', () => { tasks: [], testCases: [], occurrence: [component.minCount(), component.maxCount()], + errorCategories: [], }; const count = component.countAppliedFilters(filters); expect(count).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).toHaveBeenCalledOnce(); + }); + }); + + 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).toHaveBeenCalledOnce(); + + component.setSortedColumn('testCaseName'); + expect(component.sortingOrder()).toBe('DESCENDING'); + expect(loadDataSpy).toHaveBeenCalledTimes(2); + }); + }); + + describe('search', () => { + beforeEach(() => { + jest.spyOn(component, 'debounceLoadData' as any).mockImplementation(() => { + component['loadData'](); + }); + }); + + it('should reset page and load data when searching', async () => { + const loadDataSpy = jest.spyOn(component, 'loadData' as any); + component.searchTerm.set('test'); + await component.search(component.searchTerm()); + expect(component.page()).toBe(1); + expect(loadDataSpy).toHaveBeenCalledOnce(); + }); + }); + + 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).toHaveBeenCalledOnce(); + }); + }); }); diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-filter-modal.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-filter-modal.component.spec.ts index 369f7c42231e..ab1a83b24b9f 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-filter-modal.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-filter-modal.component.spec.ts @@ -32,12 +32,14 @@ describe('FeedbackFilterModalComponent', () => { tasks: [], testCases: [], occurrence: [component.minCount(), component.maxCount()], + errorCategories: [], }; expect(component.filters).toEqual({ tasks: [], testCases: [], occurrence: [0, 10], + errorCategories: [], }); }); @@ -47,11 +49,13 @@ describe('FeedbackFilterModalComponent', () => { const closeSpy = jest.spyOn(activeModal, 'close'); component.filters.occurrence = [component.minCount(), component.maxCount()]; + component.filters.errorCategories = ['Student Error', 'Ares Error']; component.applyFilter(); expect(storeSpy).toHaveBeenCalledWith(component.FILTER_TASKS_KEY, []); expect(storeSpy).toHaveBeenCalledWith(component.FILTER_TEST_CASES_KEY, []); expect(storeSpy).toHaveBeenCalledWith(component.FILTER_OCCURRENCE_KEY, [0, 10]); + expect(storeSpy).toHaveBeenCalledWith(component.FILTER_ERROR_CATEGORIES_KEY, ['Student Error', 'Ares Error']); expect(emitSpy).toHaveBeenCalledOnce(); expect(closeSpy).toHaveBeenCalledOnce(); }); @@ -66,29 +70,44 @@ describe('FeedbackFilterModalComponent', () => { expect(clearSpy).toHaveBeenCalledWith(component.FILTER_TASKS_KEY); expect(clearSpy).toHaveBeenCalledWith(component.FILTER_TEST_CASES_KEY); expect(clearSpy).toHaveBeenCalledWith(component.FILTER_OCCURRENCE_KEY); + expect(clearSpy).toHaveBeenCalledWith(component.FILTER_ERROR_CATEGORIES_KEY); expect(component.filters).toEqual({ tasks: [], testCases: [], occurrence: [0, 10], + errorCategories: [], }); expect(emitSpy).toHaveBeenCalledOnce(); expect(closeSpy).toHaveBeenCalledOnce(); }); - it('should update filters when checkboxes change', () => { + it('should update filters when checkboxes change for tasks', () => { const event = { target: { checked: true, value: 'test-task' } } as unknown as Event; component.onCheckboxChange(event, 'tasks'); expect(component.filters.tasks).toEqual(['test-task']); }); - it('should remove the value from filters when checkbox is unchecked', () => { + it('should update filters when checkboxes change for errorCategories', () => { + const event = { target: { checked: true, value: 'Student Error' } } as unknown as Event; + component.onCheckboxChange(event, 'errorCategories'); + expect(component.filters.errorCategories).toEqual(['Student Error']); + }); + + it('should remove the value from filters when checkbox is unchecked for tasks', () => { component.filters.tasks = ['test-task', 'task-2']; const event = { target: { checked: false, value: 'test-task' } } as unknown as Event; component.onCheckboxChange(event, 'tasks'); expect(component.filters.tasks).toEqual(['task-2']); }); + it('should remove the value from filters when checkbox is unchecked for errorCategories', () => { + component.filters.errorCategories = ['Student Error', 'Ares Error']; + const event = { target: { checked: false, value: 'Student Error' } } as unknown as Event; + component.onCheckboxChange(event, 'errorCategories'); + expect(component.filters.errorCategories).toEqual(['Ares Error']); + }); + it('should dismiss modal when closeModal is called', () => { const dismissSpy = jest.spyOn(activeModal, 'dismiss'); component.closeModal(); From 83eda712227ac1576a721bebceee200d457ff74d Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 22:01:25 +0100 Subject: [PATCH 09/15] pagination fixed --- .../grading/feedback-analysis/feedback-analysis.component.html | 1 + 1 file changed, 1 insertion(+) 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 886025e7e46a..395da3f20a76 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 @@ -97,6 +97,7 @@

From e12e7f9474a3d0a4ff3a93e9dd45d28bfea94e15 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 22:36:15 +0100 Subject: [PATCH 10/15] client tests --- .../feedback-analysis/feedback-analysis.service.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts index 5233bfbd52bf..60b280966bb1 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 @@ -9,8 +9,8 @@ describe('FeedbackAnalysisService', () => { let httpMock: HttpTestingController; const feedbackDetailsMock: FeedbackDetail[] = [ - { detailText: 'Feedback 1', testCaseName: 'test1', count: 5, relativeCount: 25.0, taskNumber: '1', errorCategory: 'StudentError' }, - { detailText: 'Feedback 2', testCaseName: 'test2', count: 3, relativeCount: 15.0, taskNumber: '2', errorCategory: 'StudentError' }, + { detailText: 'Feedback 1', testCaseName: 'test1', count: 5, relativeCount: 25.0, taskName: '1', errorCategory: 'StudentError' }, + { detailText: 'Feedback 2', testCaseName: 'test2', count: 3, relativeCount: 15.0, taskName: '2', errorCategory: 'StudentError' }, ]; const feedbackAnalysisResponseMock = { @@ -42,11 +42,11 @@ describe('FeedbackAnalysisService', () => { sortingOrder: SortingOrder.ASCENDING, sortedColumn: 'detailText', }; - const filters = { tasks: [], testCases: [], occurrence: [] }; + const filters = { tasks: [], testCases: [], occurrence: [], errorCategories: [] }; const responsePromise = service.search(pageable, { exerciseId: 1, filters }); const req = httpMock.expectOne( - 'api/exercises/1/feedback-details?page=1&pageSize=10&searchTerm=&sortingOrder=ASCENDING&sortedColumn=detailText&filterTasks=&filterTestCases=&filterOccurrence=', + 'api/exercises/1/feedback-details?page=1&pageSize=10&searchTerm=&sortingOrder=ASCENDING&sortedColumn=detailText&filterTasks=&filterTestCases=&filterOccurrence=&filterErrorCategories=', ); expect(req.request.method).toBe('GET'); req.flush(feedbackAnalysisResponseMock); From b1068221b47c1ad39dc2111b65ac44af1996da33 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 22:59:42 +0100 Subject: [PATCH 11/15] client tests --- src/main/webapp/app/shared/sort/sort-icon.component.ts | 2 +- .../feedback-analysis/modals/feedback-modal.component.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/shared/sort/sort-icon.component.ts b/src/main/webapp/app/shared/sort/sort-icon.component.ts index 001bf9683fd4..772b369e183c 100644 --- a/src/main/webapp/app/shared/sort/sort-icon.component.ts +++ b/src/main/webapp/app/shared/sort/sort-icon.component.ts @@ -4,7 +4,7 @@ import { CommonModule } from '@angular/common'; import { faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; @Component({ - selector: 'app-sort-icon', + selector: 'jhi-sort-icon', templateUrl: './sort-icon.component.html', styleUrls: ['./sort-icon.component.scss'], standalone: true, diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-modal.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-modal.component.spec.ts index 66f0d3d33aeb..f3a6c0361e9f 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-modal.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-modal.component.spec.ts @@ -14,7 +14,7 @@ describe('FeedbackModalComponent', () => { relativeCount: 25.0, detailText: 'Some feedback detail', testCaseName: 'testCase1', - taskNumber: '1', + taskName: '1', errorCategory: 'StudentError', }; From c4c6cb0629a6ab6c48761be11e6463bbc6b479cd Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Mon, 28 Oct 2024 23:09:21 +0100 Subject: [PATCH 12/15] client tests --- .../grading/feedback-analysis/feedback-analysis.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 395da3f20a76..dfccf9299d4d 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 @@ -3,7 +3,7 @@
@if (isSortableColumn(column)) { - + }
From 2eca801fc0ede8140dc8138f20df0707a72ff1fa Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 29 Oct 2024 14:08:48 +0100 Subject: [PATCH 13/15] johannes feedback --- .../feedback-analysis.component.html | 12 ++++++------ .../feedback-analysis/feedback-analysis.component.ts | 4 ++-- .../webapp/app/shared/sort/sort-icon.component.ts | 7 ++++--- .../spec/util/shared/sort-icon.component.spec.ts | 9 +++++---- 4 files changed, 17 insertions(+), 15 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 dfccf9299d4d..bdeef41c655c 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 @@ -35,12 +35,12 @@

- - - - - - + + + + + + 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 fe4ff839b4a0..2d50dbe76cbb 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 @@ -132,9 +132,9 @@ export class FeedbackAnalysisComponent { this.loadData(); } - getSortDirection(column: string): 'asc' | 'desc' | 'none' { + getSortDirection(column: string): SortingOrder.ASCENDING | SortingOrder.DESCENDING | 'none' { if (this.sortedColumn() === column) { - return this.sortingOrder() === SortingOrder.ASCENDING ? 'asc' : 'desc'; + return this.sortingOrder() === SortingOrder.ASCENDING ? SortingOrder.ASCENDING : SortingOrder.DESCENDING; } return 'none'; } diff --git a/src/main/webapp/app/shared/sort/sort-icon.component.ts b/src/main/webapp/app/shared/sort/sort-icon.component.ts index 772b369e183c..678c8ec94cac 100644 --- a/src/main/webapp/app/shared/sort/sort-icon.component.ts +++ b/src/main/webapp/app/shared/sort/sort-icon.component.ts @@ -2,6 +2,7 @@ import { Component, computed, input } from '@angular/core'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { CommonModule } from '@angular/common'; import { faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; +import { SortingOrder } from 'app/shared/table/pageable-table'; @Component({ selector: 'jhi-sort-icon', @@ -11,11 +12,11 @@ import { faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; imports: [FontAwesomeModule, CommonModule], }) export class SortIconComponent { - direction = input.required<'asc' | 'desc' | 'none'>(); + direction = input.required(); faSortUp = faSortUp; faSortDown = faSortDown; - isAscending = computed(() => this.direction() === 'asc'); - isDescending = computed(() => this.direction() === 'desc'); + isAscending = computed(() => this.direction() === SortingOrder.ASCENDING); + isDescending = computed(() => this.direction() === SortingOrder.DESCENDING); } diff --git a/src/test/javascript/spec/util/shared/sort-icon.component.spec.ts b/src/test/javascript/spec/util/shared/sort-icon.component.spec.ts index 1703af52541a..e7a9aac70bd9 100644 --- a/src/test/javascript/spec/util/shared/sort-icon.component.spec.ts +++ b/src/test/javascript/spec/util/shared/sort-icon.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { SortIconComponent } from 'app/shared/sort/sort-icon.component'; +import { SortingOrder } from 'app/shared/table/pageable-table'; describe('SortIconComponent', () => { let component: SortIconComponent; @@ -15,15 +16,15 @@ describe('SortIconComponent', () => { component = fixture.componentInstance; }); - it('should set isAscending to true when direction is "asc"', () => { - fixture.componentRef.setInput('direction', 'asc'); + it('should set isAscending to true when direction is Ascending', () => { + fixture.componentRef.setInput('direction', SortingOrder.ASCENDING); fixture.detectChanges(); expect(component.isAscending()).toBeTrue(); expect(component.isDescending()).toBeFalse(); }); - it('should set isDescending to true when direction is "desc"', () => { - fixture.componentRef.setInput('direction', 'desc'); + it('should set isDescending to true when direction is Descending', () => { + fixture.componentRef.setInput('direction', SortingOrder.DESCENDING); fixture.detectChanges(); expect(component.isDescending()).toBeTrue(); expect(component.isAscending()).toBeFalse(); From 1c5f6da8d2acab3bac8f9cabf936c58199ba9a20 Mon Sep 17 00:00:00 2001 From: aniruddhzaveri Date: Tue, 29 Oct 2024 15:50:28 +0100 Subject: [PATCH 14/15] ramona feedback --- .../feedback-filter-modal.component.html | 39 ++++++--------- .../Modal/feedback-filter-modal.component.ts | 1 + .../Modal/feedback-modal.component.html | 26 +++------- .../Modal/feedback-modal.component.ts | 1 + .../feedback-analysis.component.html | 50 +++++++------------ .../feedback-analysis.component.ts | 7 +-- .../app/shared/sort/sort-icon.component.html | 4 +- .../app/shared/sort/sort-icon.component.scss | 5 +- 8 files changed, 49 insertions(+), 84 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html index b3c8d219d314..f0b80539c28a 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html @@ -1,10 +1,10 @@