Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programming exercises: Add error categories and categorize feedback in grading analysis #9622

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -542,73 +542,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();
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

// 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();

az108 marked this conversation as resolved.
Show resolved Hide resolved
// 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();
az108 marked this conversation as resolved.
Show resolved Hide resolved

// 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
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
Loading