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: Enhance filtering and sorting for error analysis #9315

Open
wants to merge 57 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0b1023e
first approach
az108 Sep 13, 2024
e14b18c
first working valid approach
az108 Sep 13, 2024
8e4c046
final approach and server side cleaned with server side tests adapted
az108 Sep 13, 2024
bd0ff93
cleaned client side
az108 Sep 13, 2024
5a4800c
cleaned client side and added new client tests
az108 Sep 13, 2024
0d087aa
changed server side naming
az108 Sep 13, 2024
ec9fe4d
fixed i18n files
az108 Sep 13, 2024
e7d16c7
Merge branch 'develop' into feature/programming-exercises/refactor-an…
az108 Sep 13, 2024
289d2ea
coderabbit and fixed client and server style
az108 Sep 14, 2024
f03def0
Merge branch 'develop' into feature/programming-exercises/refactor-an…
az108 Sep 14, 2024
c4b5503
Patrik feedback
az108 Sep 15, 2024
e6f41e1
Merge remote-tracking branch 'origin/feature/programming-exercises/re…
az108 Sep 15, 2024
b6ad90e
Patrik feedback
az108 Sep 16, 2024
4592cfe
Patrik feedback
az108 Sep 16, 2024
afffba3
Patrik feedback
az108 Sep 16, 2024
e0df0a8
Patrik feedback
az108 Sep 16, 2024
0b6c5fa
Patrik feedback
az108 Sep 16, 2024
4547ae5
Merge branch 'develop' into feature/programming-exercises/refactor-an…
az108 Sep 16, 2024
5a00250
Johannes feedback
az108 Sep 16, 2024
46270e9
Merge remote-tracking branch 'origin/feature/programming-exercises/re…
az108 Sep 16, 2024
fc1f68f
client test fixed
az108 Sep 16, 2024
06b0c5e
sorting removed
az108 Sep 24, 2024
c68a23e
using Page now
az108 Sep 26, 2024
3b0a4f1
working slider for modal
az108 Sep 30, 2024
857f8ab
working slider for modal
az108 Sep 30, 2024
89b2434
single option filter working
az108 Oct 1, 2024
c532903
working modal
az108 Oct 2, 2024
9e00651
added header sorting for occurrence and feedback again
az108 Oct 2, 2024
dcdacc0
Merge remote-tracking branch 'origin/develop' into feature/programmin…
az108 Oct 2, 2024
80df1c6
partially working
az108 Oct 6, 2024
02ce3cf
working but clean up missing
az108 Oct 6, 2024
0f83c13
Merge remote-tracking branch 'origin/develop' into feature/programmin…
az108 Oct 6, 2024
b1e1f23
seperated maxCount call
az108 Oct 6, 2024
b44caca
occurrence slider adjustments
az108 Oct 6, 2024
f13e75a
server tests adjusted and added
az108 Oct 6, 2024
b94ed3b
client side code adjusted
az108 Oct 6, 2024
3c479bd
client side tests added / adjusted
az108 Oct 6, 2024
02f7709
client side tests added / adjusted and coderabbit
az108 Oct 7, 2024
713c060
server style
az108 Oct 7, 2024
f74a15d
Merge branch 'develop' into feature/programming-exercises/refactor-an…
az108 Oct 7, 2024
3531d70
server style
az108 Oct 7, 2024
a4af219
Merge remote-tracking branch 'origin/feature/programming-exercises/re…
az108 Oct 7, 2024
0ca27db
client code coverage
az108 Oct 7, 2024
45f04b1
client test fixed
az108 Oct 7, 2024
38a97f6
client test adjusted
az108 Oct 7, 2024
d91b2a5
range slider adjusted
az108 Oct 7, 2024
6f2f989
range slider adjusted
az108 Oct 7, 2024
6275926
more request changes handled
az108 Oct 8, 2024
5808bf9
more request changes handled
az108 Oct 8, 2024
691fff2
client test adjusted
az108 Oct 8, 2024
2898348
Merge remote-tracking branch 'origin/develop' into feature/programmin…
az108 Oct 8, 2024
c6a55b7
server style adjusted
az108 Oct 8, 2024
cdb6442
server style adjusted
az108 Oct 8, 2024
23f045d
server tests adjusted
az108 Oct 8, 2024
098e8f7
fixed small % _ interaction in searchterm
az108 Oct 9, 2024
34d6fe6
rounded relative count for modal
az108 Oct 9, 2024
e8dd9d3
Merge branch 'develop' into feature/programming-exercises/refactor-an…
az108 Oct 15, 2024
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
@@ -0,0 +1,11 @@
package de.tum.cit.aet.artemis.assessment.dto;

import java.util.List;

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) {
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
az108 marked this conversation as resolved.
Show resolved Hide resolved
az108 marked this conversation as resolved.
Show resolved Hide resolved
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, int taskNumber) {
public record FeedbackDetailDTO(long count, double relativeCount, String detailText, String testCaseName, String taskNumber, String errorCategory) {
az108 marked this conversation as resolved.
Show resolved Hide resolved
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;

import org.apache.commons.lang3.StringUtils;
import org.hibernate.Hibernate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.data.domain.Page;
az108 marked this conversation as resolved.
Show resolved Hide resolved
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.assessment.domain.AssessmentType;
import de.tum.cit.aet.artemis.assessment.domain.Feedback;
import de.tum.cit.aet.artemis.assessment.domain.FeedbackType;
import de.tum.cit.aet.artemis.assessment.domain.LongFeedbackText;
import de.tum.cit.aet.artemis.assessment.domain.Result;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO;
import de.tum.cit.aet.artemis.assessment.repository.ComplaintRepository;
import de.tum.cit.aet.artemis.assessment.repository.ComplaintResponseRepository;
Expand All @@ -40,10 +43,13 @@
import de.tum.cit.aet.artemis.buildagent.dto.ResultBuildJob;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO;
import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.core.security.Role;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.core.util.PageUtil;
import de.tum.cit.aet.artemis.exam.domain.Exam;
import de.tum.cit.aet.artemis.exam.repository.StudentExamRepository;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
Expand All @@ -53,11 +59,14 @@
import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository;
import de.tum.cit.aet.artemis.exercise.service.ExerciseDateService;
import de.tum.cit.aet.artemis.lti.service.LtiNewResultService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase;
import de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType;
import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask;
import de.tum.cit.aet.artemis.programming.repository.BuildJobRepository;
import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository;
import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository;
import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository;
import de.tum.cit.aet.artemis.programming.repository.TemplateProgrammingExerciseParticipationRepository;
Expand Down Expand Up @@ -110,6 +119,8 @@ public class ResultService {

private final ProgrammingExerciseTaskService programmingExerciseTaskService;

private final ProgrammingExerciseRepository programmingExerciseRepository;

az108 marked this conversation as resolved.
Show resolved Hide resolved
public ResultService(UserRepository userRepository, ResultRepository resultRepository, Optional<LtiNewResultService> ltiNewResultService,
ResultWebsocketService resultWebsocketService, ComplaintResponseRepository complaintResponseRepository, RatingRepository ratingRepository,
FeedbackRepository feedbackRepository, LongFeedbackTextRepository longFeedbackTextRepository, ComplaintRepository complaintRepository,
Expand All @@ -118,7 +129,7 @@ public ResultService(UserRepository userRepository, ResultRepository resultRepos
SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository,
ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, StudentExamRepository studentExamRepository,
BuildJobRepository buildJobRepository, BuildLogEntryService buildLogEntryService, StudentParticipationRepository studentParticipationRepository,
ProgrammingExerciseTaskService programmingExerciseTaskService) {
ProgrammingExerciseTaskService programmingExerciseTaskService, ProgrammingExerciseRepository programmingExerciseRepository) {
this.userRepository = userRepository;
this.resultRepository = resultRepository;
this.ltiNewResultService = ltiNewResultService;
Expand All @@ -139,6 +150,7 @@ public ResultService(UserRepository userRepository, ResultRepository resultRepos
this.buildLogEntryService = buildLogEntryService;
this.studentParticipationRepository = studentParticipationRepository;
this.programmingExerciseTaskService = programmingExerciseTaskService;
this.programmingExerciseRepository = programmingExerciseRepository;
}

/**
Expand Down Expand Up @@ -530,31 +542,85 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) {
}

/**
* Retrieves aggregated feedback details for a given exercise, calculating relative counts based on the total number of distinct results.
* The task numbers are assigned based on the associated test case names, using the set of tasks fetched from the database.
* Retrieves paginated and filtered aggregated feedback details for a given exercise.
* <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 number is determined by matching the test case name with the tasks.
* 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.
* <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).
* <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.
*
* @param exerciseId The ID of the exercise for which feedback details should be retrieved.
* @return A list of FeedbackDetailDTO objects, each containing:
* - feedback count,
* - relative count (as a percentage of distinct results),
* - detail text,
* - test case name,
* - determined task number (based on the test case name).
* @param exerciseId The ID of the exercise for which feedback details should be retrieved.
* @param search The pageable search DTO containing page number, page size, sorting options, and a search term for filtering results.
* @param filterTasks A list of task numbers to filter the feedback details based on the associated test cases (optional).
* @param filterTestCases A list of test case names to filter the feedback details (optional).
* @param filterOccurrence An array of two strings representing the minimum and maximum occurrences to include (optional).
* @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.
*/
public List<FeedbackDetailDTO> findAggregatedFeedbackByExerciseId(long exerciseId) {
public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, SearchTermPageableSearchDTO<String> search, List<String> filterTasks, List<String> filterTestCases,
String[] filterOccurrence) {
ProgrammingExercise programmingExercise = programmingExerciseRepository.findWithTestCasesByIdElseThrow(exerciseId);

long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId);
Set<ProgrammingExerciseTask> tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId);
List<FeedbackDetailDTO> feedbackDetails = studentParticipationRepository.findAggregatedFeedbackByExerciseId(exerciseId);

return feedbackDetails.stream().map(detail -> {
double relativeCount = (detail.count() * 100.0) / distinctResultCount;
int taskNumber = tasks.stream().filter(task -> task.getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(detail.testCaseName()))).findFirst()
.map(task -> tasks.stream().toList().indexOf(task) + 1).orElse(0);
return new FeedbackDetailDTO(detail.count(), relativeCount, detail.detailText(), detail.testCaseName(), taskNumber);

List<String> testCaseNames = programmingExercise.getTestCases().stream().map(ProgrammingExerciseTestCase::getTestName).toList();

List<ProgrammingExerciseTask> tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId);
Map<String, String> taskNameToIndexMap = new HashMap<>();
for (int i = 0; i < tasks.size(); i++) {
taskNameToIndexMap.put(tasks.get(i).getTaskName(), String.valueOf(i + 1));
}
Map<String, String> taskIndexToNameMap = new HashMap<>();
for (int i = 0; i < tasks.size(); i++) {
taskIndexToNameMap.put(String.valueOf(i + 1), tasks.get(i).getTaskName());
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

List<String> filterTaskNames = filterTasks.stream().map(taskIndexToNameMap::get).filter(Objects::nonNull).toList();

long minOccurrence = 0;
long maxOccurrence = Integer.MAX_VALUE;
if (filterOccurrence.length == 2) {
minOccurrence = Integer.parseInt(filterOccurrence[0]);
maxOccurrence = Integer.parseInt(filterOccurrence[1]);
}

final var pageable = PageUtil.createDefaultPageRequest(search, PageUtil.ColumnMapping.FEEDBACK_ANALYSIS);

final Page<FeedbackDetailDTO> feedbackDetailPage = studentParticipationRepository.findFilteredFeedbackByExerciseId(exerciseId,
StringUtils.isBlank(search.getSearchTerm()) ? "" : search.getSearchTerm().toLowerCase(), filterTestCases, filterTaskNames, minOccurrence, maxOccurrence, pageable);

List<FeedbackDetailDTO> processedDetails = feedbackDetailPage.getContent().stream().map(detail -> {
String taskIndex = taskNameToIndexMap.getOrDefault(detail.taskNumber(), "0");
return new FeedbackDetailDTO(detail.count(), (detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), taskIndex, "StudentError");
}).toList();

az108 marked this conversation as resolved.
Show resolved Hide resolved
return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(processedDetails, feedbackDetailPage.getTotalPages()), feedbackDetailPage.getTotalElements(), tasks.size(),
az108 marked this conversation as resolved.
Show resolved Hide resolved
testCaseNames);
}

/**
* Retrieves the maximum feedback count for a given exercise.
* <br>
* This method calls the repository to fetch the maximum number of feedback occurrences across all feedback items for a specific exercise.
* This is used for filtering feedback based on the number of occurrences.
*
* @param exerciseId The ID of the exercise for which the maximum feedback count is to be retrieved.
* @return The maximum count of feedback occurrences for the given exercise.
*/
public long getMaxCountForExercise(long exerciseId) {
return studentParticipationRepository.findMaxCountForExercise(exerciseId);
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,22 @@

import de.tum.cit.aet.artemis.assessment.domain.Feedback;
import de.tum.cit.aet.artemis.assessment.domain.Result;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO;
import de.tum.cit.aet.artemis.assessment.dto.ResultWithPointsPerGradingCriterionDTO;
import de.tum.cit.aet.artemis.assessment.repository.ResultRepository;
import de.tum.cit.aet.artemis.assessment.service.ResultService;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO;
import de.tum.cit.aet.artemis.core.dto.SortingOrder;
import de.tum.cit.aet.artemis.core.dto.pageablesearch.SearchTermPageableSearchDTO;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.core.security.Role;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor;
import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastEditorInExercise;
import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastInstructorInExercise;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.core.util.HeaderUtil;
import de.tum.cit.aet.artemis.exam.domain.Exam;
Expand Down Expand Up @@ -280,16 +283,61 @@ public ResponseEntity<Result> createResultForExternalSubmission(@PathVariable Lo
}

/**
* GET /exercises/:exerciseId/feedback-details : Retrieves all aggregated feedback details for a given exercise.
* The feedback details include counts and relative counts of feedback occurrences, along with associated test case names and task numbers.
* GET /exercises/{exerciseId}/feedback-details-paged : Retrieves paginated and filtered aggregated feedback details for a given exercise.
* The feedback details include counts and relative counts of feedback occurrences, test case names, and task numbers.
* The method allows filtering by a search term and sorting by various fields.
* <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".
* <br>
* The response contains both the paginated feedback details and the total count of distinct results for the exercise.
*
* @param exerciseId The ID of the exercise for which feedback details should be retrieved.
* @return A ResponseEntity containing a list of {@link FeedbackDetailDTO}s
* @param exerciseId The ID of the exercise for which feedback details should be retrieved.
* @param page The page number of the results to retrieve (0-indexed).
* @param pageSize The number of feedback details per page.
* @param searchTerm The search term used for filtering the results (optional).
* @param sortingOrder The sorting order for the results (ASCENDING or DESCENDING).
* @param sortedColumn The column by which the results should be sorted. If an invalid column (e.g., "taskNumber", "errorCategory") is provided, sorting defaults to
* "count".
* @param filterTasks A list of task numbers to filter feedback details by the associated tasks (optional).
* @param filterTestCases A list of test case names to filter feedback details by the associated test cases (optional).
* @param filterOccurrence An array of two values representing the minimum and maximum occurrence of feedback items to include (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<String> testCaseNames: A list of test case names included in the feedback.
*/
@GetMapping("exercises/{exerciseId}/feedback-details")
@EnforceAtLeastEditorInExercise
public ResponseEntity<List<FeedbackDetailDTO>> getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) {
log.debug("REST request to get all Feedback details for Exercise {}", exerciseId);
return ResponseEntity.ok(resultService.findAggregatedFeedbackByExerciseId(exerciseId));
@GetMapping("exercises/{exerciseId}/feedback-details-paged")
@EnforceAtLeastInstructorInExercise
public ResponseEntity<FeedbackAnalysisResponseDTO> getFeedbackDetailsPaged(@PathVariable long exerciseId, @RequestParam int page, @RequestParam int pageSize,
@RequestParam String searchTerm, @RequestParam SortingOrder sortingOrder, @RequestParam String sortedColumn, @RequestParam List<String> filterTasks,
@RequestParam List<String> filterTestCases, @RequestParam String[] filterOccurrence) {

SearchTermPageableSearchDTO<String> search = new SearchTermPageableSearchDTO<>();
search.setPage(page);
search.setPageSize(pageSize);
search.setSearchTerm(searchTerm);
search.setSortingOrder(sortingOrder);
search.setSortedColumn(sortedColumn);

FeedbackAnalysisResponseDTO response = resultService.getFeedbackDetailsOnPage(exerciseId, search, filterTasks, filterTestCases, filterOccurrence);
return ResponseEntity.ok(response);
}

/**
* GET /exercises/{exerciseId}/feedback-details-max-count : Retrieves the maximum number of feedback occurrences for a given exercise.
* This method is useful for determining the highest count of feedback occurrences across all feedback items for the exercise,
* which can then be used to filter or adjust feedback analysis results.
*
* @param exerciseId The ID of the exercise for which the maximum feedback count should be retrieved.
* @return A {@link ResponseEntity} containing the maximum count of feedback occurrences (long).
*/
@GetMapping("exercises/{exerciseId}/feedback-details-max-count")
@EnforceAtLeastInstructorInExercise
public ResponseEntity<Long> getMaxCount(@PathVariable long exerciseId) {
long maxCount = resultService.getMaxCountForExercise(exerciseId);
return ResponseEntity.ok(maxCount);
}
}
Loading
Loading