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 affected students to feedback analysis table #9728

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -0,0 +1,4 @@
package de.tum.cit.aet.artemis.assessment.dto;

public record FeedbackAffectedStudentDTO(long courseId, long participationId, String firstName, String lastName, String login, String repositoryName) {
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, String taskName, String errorCategory) {
public record FeedbackDetailDTO(Object concatenatedFeedbackIds, long count, double relativeCount, String detailText, String testCaseName, String taskName, 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 @@ -12,7 +12,6 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import jakarta.annotation.Nullable;
Expand All @@ -24,13 +23,15 @@
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
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.FeedbackAffectedStudentDTO;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackPageableDTO;
Expand All @@ -46,6 +47,7 @@
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.PageableSearchDTO;
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;
Expand Down Expand Up @@ -495,49 +497,45 @@ public Map<Long, String> getLogsAvailabilityForResults(List<Result> results, Par

@NotNull
private List<Feedback> saveFeedbackWithHibernateWorkaround(@NotNull Result result, List<Feedback> feedbackList) {
// Avoid hibernate exception
List<Feedback> savedFeedbacks = new ArrayList<>();
// Collect ids of feedbacks that have long feedback.
List<Long> feedbackIdsWithLongFeedback = feedbackList.stream().filter(feedback -> feedback.getId() != null && feedback.getHasLongFeedbackText()).map(Feedback::getId)
.toList();
// Get long feedback list from the database.
List<LongFeedbackText> longFeedbackTextList = longFeedbackTextRepository.findByFeedbackIds(feedbackIdsWithLongFeedback);

// Fetch long feedback texts associated with the provided feedback list
Map<Long, LongFeedbackText> longFeedbackTextMap = longFeedbackTextRepository
.findByFeedbackIds(feedbackList.stream().filter(feedback -> feedback.getId() != null && feedback.getHasLongFeedbackText()).map(Feedback::getId).toList()).stream()
.collect(Collectors.toMap(longFeedbackText -> longFeedbackText.getFeedback().getId(), Function.identity()));

// Convert list to map for accessing later.
Map<Long, LongFeedbackText> longLongFeedbackTextMap = longFeedbackTextList.stream()
.collect(Collectors.toMap(longFeedbackText -> longFeedbackText.getFeedback().getId(), longFeedbackText -> longFeedbackText));
feedbackList.forEach(feedback -> {
handleFeedbackPersistence(feedback, result, longFeedbackTextMap);
savedFeedbacks.add(feedback);
});

return savedFeedbacks;
}

private void handleFeedbackPersistence(Feedback feedback, Result result, Map<Long, LongFeedbackText> longFeedbackTextMap) {
// Temporarily detach feedback from the parent result to avoid Hibernate issues
feedback.setResult(null);
// cut association to parent object
feedback.setResult(null);
LongFeedbackText longFeedback = null;
// look for long feedback that parent feedback has and cut the association between them.
if (feedback.getId() != null && feedback.getHasLongFeedbackText()) {
longFeedback = longLongFeedbackTextMap.get(feedback.getId());
if (longFeedback != null) {
feedback.clearLongFeedback();
}
}
// persist the child object without an association to the parent object.
feedback = feedbackRepository.saveAndFlush(feedback);
// restore the association to the parent object
feedback.setResult(result);

// Clear the long feedback if it exists in the map
if (feedback.getId() != null && feedback.getHasLongFeedbackText()) {
LongFeedbackText longFeedback = longFeedbackTextMap.get(feedback.getId());
// restore the association of the long feedback to the parent feedback
if (longFeedback != null) {
feedback.clearLongFeedback();
feedback.setDetailText(longFeedback.getText());
}
}

// Persist the feedback entity without the parent association
feedback = feedbackRepository.saveAndFlush(feedback);

// Restore associations to the result and long feedback after persistence
feedback.setResult(result);
LongFeedbackText longFeedback = longFeedbackTextMap.get(feedback.getId());
if (longFeedback != null) {
feedback.setDetailText(longFeedback.getText());
}
savedFeedbacks.add(feedback);
});
return savedFeedbacks;
}

@NotNull
private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) {
if (shouldSave) {
// long feedback text is deleted as it otherwise causes duplicate entries errors and will be saved again with {@link resultRepository.save}
deleteLongFeedback(result.getFeedbacks(), result);
// Note: This also saves the feedback objects in the database because of the 'cascade = CascadeType.ALL' option.
return resultRepository.save(result);
}
Expand Down Expand Up @@ -618,8 +616,10 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee
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();
List<FeedbackDetailDTO> processedDetails = feedbackDetailPage.getContent().stream()
.map(detail -> new FeedbackDetailDTO(List.of(String.valueOf(detail.concatenatedFeedbackIds()).split(",")), 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");
Expand All @@ -643,31 +643,33 @@ public long getMaxCountForExercise(long exerciseId) {
}

/**
* Deletes long feedback texts for the provided list of feedback items to prevent duplicate entries in the {@link LongFeedbackTextRepository}.
* Retrieves a paginated list of students affected by specific feedback entries for a given exercise.
* <br>
* This method processes the provided list of feedback items, identifies those with associated long feedback texts, and removes them in bulk
* from the repository to avoid potential duplicate entry errors when saving new feedback entries.
* <p>
* Primarily used to ensure data consistency in the {@link LongFeedbackTextRepository}, especially during operations where feedback entries are
* overridden or updated. The deletion is performed only for feedback items with a non-null ID and an associated long feedback text.
* <p>
* This approach reduces the need for individual deletion calls and performs batch deletion in a single database operation.
* <p>
* **Note:** This method should only be used for manually assessed submissions, not for fully automatic assessments, due to its dependency on the
* {@link Result#updateAllFeedbackItems} method, which is designed for manual feedback management. Using this method with automatic assessments could
* lead to unintended behavior or data inconsistencies.
* This method filters students based on feedback IDs and returns participation details for each affected student. It uses
* pagination and sorting to allow efficient retrieval and sorting of the results, thus supporting large datasets.
* <br>
* Supports:
* <ul>
* <li><b>Pagination:</b> Controls the page number and page size for the returned results.</li>
* <li><b>Sorting:</b> Applies sorting by specified columns and sorting order based on the {@link PageUtil.ColumnMapping#AFFECTED_STUDENTS} configuration.</li>
* </ul>
*
* @param feedbackList The list of {@link Feedback} objects for which the long feedback texts are to be deleted. Only feedback items that have long feedback texts and a
* non-null ID will be processed.
* @param result The {@link Result} object associated with the feedback items, used to update feedback list before processing.
* @param exerciseId The ID of the exercise for which the affected student participation data is requested.
* @param feedbackIds A list of feedback IDs used to filter the participation to only those affected by specific feedback entries.
* @param data A {@link PageableSearchDTO} object containing pagination and sorting parameters:
* <ul>
* <li>Page number and page size for pagination</li>
* <li>Sorting order and column for sorting (e.g., "participationId")</li>
* </ul>
* @return A {@link Page} of {@link FeedbackAffectedStudentDTO} objects, each representing a student affected by the feedback, with:
* <ul>
* <li>Details about the affected students, including participation data and student information.</li>
* <li>Total count of affected students, allowing for pagination on the client side.</li>
* </ul>
*/
public void deleteLongFeedback(List<Feedback> feedbackList, Result result) {
if (feedbackList == null) {
return;
}
List<Long> feedbackIdsWithLongText = feedbackList.stream().filter(feedback -> feedback.getHasLongFeedbackText() && feedback.getId() != null).map(Feedback::getId).toList();
longFeedbackTextRepository.deleteByFeedbackIds(feedbackIdsWithLongText);
List<Feedback> feedbacks = new ArrayList<>(feedbackList);
result.updateAllFeedbackItems(feedbacks, true);
public Page<FeedbackAffectedStudentDTO> getParticipationWithFeedbackId(long exerciseId, List<String> feedbackIds, PageableSearchDTO<String> data) {
List<Long> feedbackIdLongs = feedbackIds.stream().map(Long::valueOf).toList();
PageRequest pageRequest = PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.AFFECTED_STUDENTS);
return studentParticipationRepository.findParticipationByFeedbackId(exerciseId, feedbackIdLongs, pageRequest);
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand All @@ -28,6 +29,7 @@

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.FeedbackAffectedStudentDTO;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackPageableDTO;
import de.tum.cit.aet.artemis.assessment.dto.ResultWithPointsPerGradingCriterionDTO;
Expand All @@ -36,13 +38,14 @@
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.PageableSearchDTO;
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.EnforceAtLeastInstructorInExercise;
import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastEditorInExercise;
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 @@ -328,7 +331,7 @@ public ResponseEntity<Result> createResultForExternalSubmission(@PathVariable Lo
* </ul>
*/
@GetMapping("exercises/{exerciseId}/feedback-details")
@EnforceAtLeastInstructorInExercise
@EnforceAtLeastEditorInExercise
public ResponseEntity<FeedbackAnalysisResponseDTO> getFeedbackDetailsPaged(@PathVariable long exerciseId, @ModelAttribute FeedbackPageableDTO data) {
FeedbackAnalysisResponseDTO response = resultService.getFeedbackDetailsOnPage(exerciseId, data);
return ResponseEntity.ok(response);
Expand All @@ -343,9 +346,42 @@ public ResponseEntity<FeedbackAnalysisResponseDTO> getFeedbackDetailsPaged(@Path
* @return A {@link ResponseEntity} containing the maximum count of feedback occurrences (long).
*/
@GetMapping("exercises/{exerciseId}/feedback-details-max-count")
@EnforceAtLeastInstructorInExercise
@EnforceAtLeastEditorInExercise
public ResponseEntity<Long> getMaxCount(@PathVariable long exerciseId) {
long maxCount = resultService.getMaxCountForExercise(exerciseId);
return ResponseEntity.ok(maxCount);
}

/**
* GET /exercises/{exerciseId}/feedback-details-participation : Retrieves paginated details of students affected by specific feedback entries for a specified exercise.
* <br>
* This endpoint returns details of students whose submissions were impacted by specified feedback entries, including student information
* and participation details. This supports efficient loading of affected students' data by returning only the required information.
* <br>
* Supports pagination and sorting, allowing the client to control the amount and order of returned data:
* <ul>
* <li><b>Pagination:</b> Controls page number and page size for the response.</li>
* <li><b>Sorting:</b> Allows sorting by specified columns (e.g., "participationId") in ascending or descending order.</li>
* </ul>
*
* @param exerciseId The ID of the exercise for which the participation data is requested.
* @param feedbackIds A list of feedback IDs to filter affected students by specific feedback entries.
* @param data A {@link PageableSearchDTO} object containing pagination and sorting parameters:
* <ul>
* <li>Page number and page size</li>
* <li>Sorted column and sorting order</li>
* </ul>
* @return A {@link ResponseEntity} containing a {@link Page} of {@link FeedbackAffectedStudentDTO}, each representing a student affected by the feedback entries, including:
* <ul>
* <li>List of affected students with participation details.</li>
* <li>Total number of students affected (for pagination).</li>
* </ul>
*/
az108 marked this conversation as resolved.
Show resolved Hide resolved
@GetMapping("exercises/{exerciseId}/feedback-details-participation")
@EnforceAtLeastEditorInExercise
public ResponseEntity<Page<FeedbackAffectedStudentDTO>> getParticipationWithFeedback(@PathVariable long exerciseId, @RequestParam List<String> feedbackIds,
az108 marked this conversation as resolved.
Show resolved Hide resolved
az108 marked this conversation as resolved.
Show resolved Hide resolved
@ModelAttribute PageableSearchDTO<String> data) {
Page<FeedbackAffectedStudentDTO> participation = resultService.getParticipationWithFeedbackId(exerciseId, feedbackIds, data);
return ResponseEntity.ok(participation);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ SELECT MAX(t.taskName)
JOIN t.testCases tct
WHERE t.exercise.id = :exerciseId AND tct.testName = f.testCase.testName
), '')"""
)),
AFFECTED_STUDENTS(Map.of(
"participationId", "p.id"
));
// @formatter:on

Expand Down
Loading
Loading