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 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
@@ -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 repositoryURI) {
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package de.tum.cit.aet.artemis.assessment.dto;

import java.util.Arrays;
import java.util.List;

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(List<Long> concatenatedFeedbackIds, long count, double relativeCount, String detailText, String testCaseName, String taskName,
String errorCategory) {

public FeedbackDetailDTO(String concatenatedFeedbackIds, long count, double relativeCount, String detailText, String testCaseName, String taskName, String errorCategory) {
this(Arrays.stream(concatenatedFeedbackIds.split(",")).map(Long::valueOf).toList(), count, relativeCount, detailText, testCaseName, taskName, errorCategory);
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
Expand All @@ -24,13 +25,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 +49,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 @@ -618,9 +622,8 @@ 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(),
List<FeedbackDetailDTO> processedDetails = feedbackDetailPage.getContent().stream().map(detail -> new FeedbackDetailDTO(detail.concatenatedFeedbackIds(), detail.count(),
az108 marked this conversation as resolved.
Show resolved Hide resolved
az108 marked this conversation as resolved.
Show resolved Hide resolved
(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");

Expand All @@ -642,6 +645,25 @@ public long getMaxCountForExercise(long exerciseId) {
return studentParticipationRepository.findMaxCountForExercise(exerciseId);
}

/**
* Retrieves a paginated list of students affected by specific feedback entries for a given exercise.
* <br>
* This method filters students based on feedback IDs and returns participation details for each affected student. It uses
* pagination and sorting (order based on the {@link PageUtil.ColumnMapping#AFFECTED_STUDENTS}) to allow efficient retrieval and sorting of the results, thus supporting large
* datasets.
* <br>
*
* @param exerciseId for which the affected student participation data is requested.
* @param feedbackIds used to filter the participation to only those affected by specific feedback entries.
* @param data A {@link PageableSearchDTO} object containing pagination and sorting parameters.
* @return A {@link Page} of {@link FeedbackAffectedStudentDTO} objects, each representing a student affected by the feedback.
*/
public Page<FeedbackAffectedStudentDTO> getAffectedStudentsWithFeedbackId(long exerciseId, String feedbackIds, PageableSearchDTO<String> data) {
List<Long> feedbackIdLongs = Arrays.stream(feedbackIds.split(",")).map(Long::valueOf).toList();
PageRequest pageRequest = PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.AFFECTED_STUDENTS);
return studentParticipationRepository.findAffectedStudentsByFeedbackId(exerciseId, feedbackIdLongs, pageRequest);
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Deletes long feedback texts for the provided list of feedback items to prevent duplicate entries in the {@link LongFeedbackTextRepository}.
* <br>
Expand Down
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 @@ -22,12 +23,14 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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 +39,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 +332,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 +347,30 @@ 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.
* This endpoint returns details of students whose submissions were impacted by specified feedback entries, including student information
* and participation details.
* <br>
*
* @param exerciseId for which the participation data is requested.
* @param feedbackIdsHeader to filter affected students by specific feedback entries.
* @param data A {@link PageableSearchDTO} object containing pagination and sorting parameters.
* @return A {@link ResponseEntity} containing a {@link Page} of {@link FeedbackAffectedStudentDTO}, each representing a student affected by the feedback entries.
*/
@GetMapping("exercises/{exerciseId}/feedback-details-participation")
@EnforceAtLeastEditorInExercise
public ResponseEntity<Page<FeedbackAffectedStudentDTO>> getAffectedStudentsWithFeedback(@PathVariable long exerciseId, @RequestHeader("feedbackIds") String feedbackIdsHeader,
@ModelAttribute PageableSearchDTO<String> data) {

Page<FeedbackAffectedStudentDTO> participation = resultService.getAffectedStudentsWithFeedbackId(exerciseId, feedbackIdsHeader, 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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import de.tum.cit.aet.artemis.assessment.domain.AssessmentType;
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.FeedbackDetailDTO;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;
Expand Down Expand Up @@ -1217,7 +1218,7 @@ SELECT COALESCE(AVG(p.presentationScore), 0)
* - 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".
* <br>
* Grouping is done by feedback detail text, test case name, and error category. 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.
az108 marked this conversation as resolved.
Show resolved Hide resolved
*
* @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).
Expand All @@ -1233,14 +1234,15 @@ SELECT COALESCE(AVG(p.presentationScore), 0)
*/
@Query("""
SELECT new de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO(
LISTAGG(CAST(f.id AS string), ',') WITHIN GROUP (ORDER BY f.id),
COUNT(f.id),
0,
f.detailText,
f.testCase.testName,
COALESCE((
SELECT MAX(t.taskName)
FROM ProgrammingExerciseTask t
JOIN t.testCases tct
LEFT JOIN t.testCases tct
az108 marked this conversation as resolved.
Show resolved Hide resolved
WHERE t.exercise.id = :exerciseId AND tct.testName = f.testCase.testName
), 'Not assigned to task'),
CASE
Expand All @@ -1250,12 +1252,12 @@ SELECT MAX(t.taskName)
END
)
FROM StudentParticipation p
JOIN p.results r ON r.id = (
LEFT 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
LEFT JOIN r.feedbacks f
WHERE p.exercise.id = :exerciseId
AND p.testRun = FALSE
AND f.positive = FALSE
Expand All @@ -1264,7 +1266,7 @@ SELECT MAX(pr.id)
AND (:#{#filterTaskNames != NULL && #filterTaskNames.size() < 1} = TRUE OR f.testCase.testName NOT IN (
SELECT tct.testName
FROM ProgrammingExerciseTask t
JOIN t.testCases tct
LEFT JOIN t.testCases tct
WHERE t.taskName IN (:filterTaskNames)
))
AND (:#{#filterErrorCategories != NULL && #filterErrorCategories.size() < 1} = TRUE OR CASE
Expand All @@ -1290,7 +1292,7 @@ Page<FeedbackDetailDTO> findFilteredFeedbackByExerciseId(@Param("exerciseId") lo
@Query("""
SELECT COUNT(DISTINCT r.id)
FROM StudentParticipation p
JOIN p.results r ON r.id = (
LEFT JOIN p.results r ON r.id = (
SELECT MAX(pr.id)
FROM p.results pr
WHERE pr.participation.id = p.id
Expand All @@ -1317,17 +1319,46 @@ SELECT MAX(feedbackCounts.feedbackCount)
FROM (
SELECT COUNT(f.id) AS feedbackCount
FROM StudentParticipation p
JOIN p.results r ON r.id = (
LEFT 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
LEFT JOIN r.feedbacks f
WHERE p.exercise.id = :exerciseId
AND p.testRun = FALSE
AND f.positive = FALSE
GROUP BY f.detailText, f.testCase.testName
) AS feedbackCounts
""")
long findMaxCountForExercise(@Param("exerciseId") long exerciseId);

/**
* Retrieves a paginated list of students affected by specific feedback entries for a given programming exercise.
* <br>
*
* @param exerciseId for which the affected student participation data is requested.
* @param feedbackIds used to filter the participation to only those affected by specific feedback entries.
* @param pageable A {@link Pageable} object to control pagination and sorting of the results, specifying page number, page size, and sort order.
* @return A {@link Page} of {@link FeedbackAffectedStudentDTO} objects, each representing a student affected by the feedback.
*/
@Query("""
SELECT new de.tum.cit.aet.artemis.assessment.dto.FeedbackAffectedStudentDTO(
p.exercise.course.id,
p.id,
p.student.firstName,
p.student.lastName,
p.student.login,
p.repositoryUri
)
FROM ProgrammingExerciseStudentParticipation p
LEFT JOIN p.submissions s
LEFT JOIN s.results r
LEFT JOIN r.feedbacks f
WHERE p.exercise.id = :exerciseId
AND f.id IN :feedbackIds
AND p.testRun = FALSE
ORDER BY p.student.firstName ASC
""")
Page<FeedbackAffectedStudentDTO> findAffectedStudentsByFeedbackId(@Param("exerciseId") long exerciseId, @Param("feedbackIds") List<Long> feedbackIds, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<ng-template #headerTemplate let-label="label" let-colSize="colSize">
<th scope="col" [class]="colSize">
<span [jhiTranslate]="label"></span>
</th>
</ng-template>
<div class="modal-header">
<h4 class="modal-title" [jhiTranslate]="TRANSLATION_BASE + '.header'"></h4>
<button type="button" class="btn-close" aria-label="Close" (click)="activeModal.dismiss()"></button>
</div>
<div class="modal-body">
<table class="table table-striped mb-3">
<colgroup>
<col class="col" />
<col class="col" />
<col class="col" />
</colgroup>
az108 marked this conversation as resolved.
Show resolved Hide resolved
<thead>
<tr>
<ng-container *ngTemplateOutlet="headerTemplate; context: { label: TRANSLATION_BASE + '.name', colSize: 'col-4' }"></ng-container>
<ng-container *ngTemplateOutlet="headerTemplate; context: { label: TRANSLATION_BASE + '.login', colSize: 'col-4' }"></ng-container>
<ng-container *ngTemplateOutlet="headerTemplate; context: { label: TRANSLATION_BASE + '.repository', colSize: 'col-2' }"></ng-container>
</tr>
</thead>
<tbody class="table-group-divider">
@for (item of participation().content; track item) {
<tr>
<td>{{ item.firstName }} {{ item.lastName }}</td>
<td>{{ item.login }}</td>
<td>
<jhi-code-button
class="ms-2"
[smallButtons]="true"
[repositoryUri]="item.repositoryURI"
[routerLinkForRepositoryView]="['/courses', item.courseId, 'exercises', exerciseId(), 'repository', item.participationId]"
/>
</td>
</tr>
}
</tbody>
</table>
<div class="d-flex flex-column align-items-end mt-2">
@if (participation().totalElements >= pageSize()) {
<ngb-pagination [collectionSize]="collectionsSize()" [pageSize]="pageSize()" [page]="page()" (pageChange)="setPage($event)" size="sm" [maxSize]="5"></ngb-pagination>
}
<div class="text-muted text-end">
<span [jhiTranslate]="TRANSLATION_BASE + '.totalItems'" [translateValues]="{ count: participation().totalElements }"></span>
</div>
</div>
</div>
Loading
Loading