Skip to content

Commit

Permalink
Programming exercises: Add error categories and categorize feedback i…
Browse files Browse the repository at this point in the history
…n grading analysis (#9622)
  • Loading branch information
az108 authored Nov 10, 2024
1 parent e89f035 commit 395e8df
Show file tree
Hide file tree
Showing 25 changed files with 498 additions and 279 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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<FeedbackDetailDTO> feedbackDetails, long totalItems, int totalAmountOfTasks, List<String> testCaseNames) {
public record FeedbackAnalysisResponseDTO(SearchResultPageDTO<FeedbackDetailDTO> feedbackDetails, long totalItems, Set<String> taskNames, List<String> testCaseNames,
List<String> errorCategories) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public class FeedbackPageableDTO extends PageableSearchDTO<String> {

private String searchTerm;

private List<String> filterErrorCategories;

public List<String> getFilterTasks() {
return filterTasks;
}
Expand Down Expand Up @@ -45,4 +47,12 @@ public String getSearchTerm() {
public void setSearchTerm(String searchTerm) {
this.searchTerm = searchTerm;
}

public List<String> getFilterErrorCategories() {
return filterErrorCategories;
}

public void setFilterErrorCategories(List<String> filterErrorCategories) {
this.filterErrorCategories = filterErrorCategories;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -547,73 +547,86 @@ 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, task name, and error category.
* <br>
* 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."
* 3. Error categories are classified as one of "Student Error," "Ares Error," or "AST Error," based on feedback content.
* <br>
* 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.
* - Error categories: Filters feedback based on selected error categories, such as "Student Error," "Ares Error," and "AST Error."
* <br>
* 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,
* 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.
* - 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.
* - 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 test case names using streams
List<String> testCaseNames = programmingExercise.getTestCases().stream().map(ProgrammingExerciseTestCase::getTestName).toList();
// 3. Extract only active test case names for use in filtering options
List<String> activeTestCaseNames = programmingExercise.getTestCases().stream().filter(ProgrammingExerciseTestCase::isActive).map(ProgrammingExerciseTestCase::getTestName)
.toList();

// 4. Retrieve all tasks associated with the exercise and map their names
List<ProgrammingExerciseTask> tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId);
Set<String> taskNames = tasks.stream().map(ProgrammingExerciseTask::getTaskName).collect(Collectors.toSet());

// 3. Generate filter task names directly
List<String> 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();
// 5. Include unassigned tasks if specified by the filter; otherwise, only include specified tasks
List<String> includeUnassignedTasks = new ArrayList<>(taskNames);
if (!data.getFilterTasks().isEmpty()) {
includeUnassignedTasks.removeAll(data.getFilterTasks());
}
else {
includeUnassignedTasks.clear();
}

// 4. Set minOccurrence and maxOccurrence based on filterOccurrence
// 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. Create pageable object for pagination
// 7. Define the error categories to filter based on user selection
List<String> 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 from the repository
// 9. Query the database to retrieve paginated and filtered feedback
final Page<FeedbackDetailDTO> feedbackDetailPage = studentParticipationRepository.findFilteredFeedbackByExerciseId(exerciseId,
StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), filterTaskNames, minOccurrence, maxOccurrence,
pageable);

// 7. Process feedback details
// Map to index (+1 for 1-based indexing)
List<FeedbackDetailDTO> 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");
}).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);
StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), includeUnassignedTasks, minOccurrence,
maxOccurrence, filterErrorCategories, pageable);

// 10. Process and map feedback details, calculating relative count and assigning task names
List<FeedbackDetailDTO> processedDetails = feedbackDetailPage.getContent().stream().map(detail -> new FeedbackDetailDTO(detail.count(),
(detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), detail.taskName(), detail.errorCategory())).toList();

// 11. Predefined error categories available for filtering on the client side
final List<String> 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, ERROR_CATEGORIES);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,36 +283,49 @@ public ResponseEntity<Result> 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.
* <br>
* 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.
* <br>
* Filtering is applied based on:
* - Task numbers (mapped to task names)
* - Test case names
* - Occurrence range (minimum and maximum occurrences)
* <br>
* 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:
* <ul>
* <li><b>Pagination:</b> Based on page number and page size, as specified in the request.</li>
* <li><b>Sorting:</b> 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".</li>
* <li><b>Filtering:</b>
* <ul>
* <li><b>Task names:</b> Filters feedback entries by specific task names, including "Not assigned to task" if unassigned feedback is requested.</li>
* <li><b>Test case names:</b> Filters feedback by specified test cases, using only active test cases from the exercise.</li>
* <li><b>Occurrence range:</b> Filters by the minimum and maximum number of occurrences (inclusive).</li>
* <li><b>Search term:</b> Case-insensitive filter applied to feedback detail text.</li>
* <li><b>Error categories:</b> Filters feedback entries by specified error categories (e.g., "Student Error," "Ares Error," and "AST Error").</li>
* </ul>
* </li>
* </ul>
*
* @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:
* <ul>
* <li>Page number and page size</li>
* <li>Search term (optional)</li>
* <li>Sorting order (ASCENDING or DESCENDING)</li>
* <li>Sorted column</li>
* <li>Filter task names (optional)</li>
* <li>Filter test case names (optional)</li>
* <li>Occurrence range (optional)</li>
* <li>Error categories (optional)</li>
* </ul>
* @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<String> testCaseNames: A list of test case names included in the feedback.
* <ul>
* <li>{@link SearchResultPageDTO < FeedbackDetailDTO >} feedbackDetails: Paginated and filtered feedback details for the exercise.</li>
* <li>long totalItems: The total count of feedback entries (for pagination).</li>
* <li>Set<String> taskNames: A set of task names relevant to the feedback items, including "Not assigned to task" if applicable.</li>
* <li>List<String> testCaseNames: A list of active test case names used in the feedback.</li>
* <li>List<String> errorCategories: The list of error categories included in the feedback details, such as "Student Error," "Ares Error," and "AST Error".</li>
* </ul>
*/
@GetMapping("exercises/{exerciseId}/feedback-details")
@EnforceAtLeastInstructorInExercise
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit 395e8df

Please sign in to comment.