Skip to content

Commit

Permalink
Programming exercises: Add affected students to feedback analysis tab…
Browse files Browse the repository at this point in the history
…le (#9728)
  • Loading branch information
az108 authored Nov 16, 2024
1 parent d3311ae commit 7896ff9
Show file tree
Hide file tree
Showing 19 changed files with 486 additions and 49 deletions.
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);
}

}
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(),
(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);
}

/**
* 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);
}
}
3 changes: 3 additions & 0 deletions src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java
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.
*
* @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
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>
<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

0 comments on commit 7896ff9

Please sign in to comment.