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 2 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 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 @@ -24,13 +24,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 +48,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 +621,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 @@ -642,6 +646,37 @@ 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 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 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>
*/
az108 marked this conversation as resolved.
Show resolved Hide resolved
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
}

/**
* 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 @@ -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
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,6 +1234,7 @@ SELECT COALESCE(AVG(p.presentationScore), 0)
*/
@Query("""
SELECT new de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO(
GROUP_CONCAT(f.id),
COUNT(f.id),
0,
f.detailText,
Expand Down Expand Up @@ -1329,4 +1331,46 @@ SELECT MAX(pr.id)
) 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>
* This query joins `ProgrammingExerciseStudentParticipation`, `Submission`, `Result`, and `Feedback` entities to filter
* participation records based on feedback IDs and exercise ID. The results include information about each affected student,
* such as their course ID, participation ID, student details, and repository URI.
* <br>
* Supports:
* <ul>
* <li><b>Pagination:</b> The results are paginated using a {@link Pageable} parameter, allowing control over the page number and size.</li>
* <li><b>Sorting:</b> The query sorts results by the student's first name in ascending order.</li>
* </ul>
*
* @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 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, containing:
* <ul>
* <li>Course ID, participation ID, and student information (first name, last name, login).</li>
* <li>Repository URI, linking the affected student's repository.</li>
* <li>Total count of affected students to facilitate pagination on the client side.</li>
* </ul>
az108 marked this conversation as resolved.
Show resolved Hide resolved
*/
@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
JOIN s.results r
JOIN r.feedbacks f
WHERE p.exercise.id = :exerciseId
AND f.id IN :feedbackIds
ORDER BY p.student.firstName ASC
""")
Page<FeedbackAffectedStudentDTO> findParticipationByFeedbackId(@Param("exerciseId") long exerciseId, @Param("feedbackIds") List<Long> feedbackIds, Pageable pageable);
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
@@ -0,0 +1,42 @@
<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>
<th [jhiTranslate]="TRANSLATION_BASE + '.name'"></th>
<th [jhiTranslate]="TRANSLATION_BASE + '.login'"></th>
<th [jhiTranslate]="TRANSLATION_BASE + '.repository'"></th>
</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.repositoryName"
[routerLinkForRepositoryView]="['/courses', item.courseId, 'exercises', exerciseId(), 'repository', item.participationId]"
/>
</td>
</tr>
}
</tbody>
</table>
<div class="d-flex flex-column align-items-end mt-2">
<ngb-pagination [collectionSize]="collectionsSize()" [pageSize]="pageSize()" [page]="page()" (pageChange)="setPage($event)" size="sm" [maxSize]="5"></ngb-pagination>
az108 marked this conversation as resolved.
Show resolved Hide resolved
<div class="text-muted text-end">
<span [jhiTranslate]="TRANSLATION_BASE + '.totalItems'" [translateValues]="{ count: participation().totalElements }"></span>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Component, computed, effect, inject, input, signal, untracked } from '@angular/core';
import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module';
import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module';
import { FeedbackAffectedStudentDTO, FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { AlertService } from 'app/core/util/alert.service';
import { PageableResult, PageableSearch, SortingOrder } from 'app/shared/table/pageable-table';

@Component({
selector: 'jhi-affected-students-modal',
templateUrl: './feedback-affected-students-modal.component.html',
imports: [ArtemisSharedCommonModule, ArtemisSharedComponentModule],
providers: [FeedbackAnalysisService],
standalone: true,
})
export class AffectedStudentsModalComponent {
exerciseId = input.required<number>();
feedbackDetail = input.required<FeedbackDetail>();
readonly participation = signal<PageableResult<FeedbackAffectedStudentDTO>>({ content: [], totalPages: 0, totalElements: 0 });
readonly TRANSLATION_BASE = 'artemisApp.programmingExercise.configureGrading.feedbackAnalysis.affectedStudentsModal';

page = signal<number>(1);
pageSize = signal<number>(10);
readonly collectionsSize = computed(() => this.participation().totalPages * this.pageSize());
az108 marked this conversation as resolved.
Show resolved Hide resolved

activeModal = inject(NgbActiveModal);
feedbackService = inject(FeedbackAnalysisService);
alertService = inject(AlertService);

constructor() {
effect(() => {
untracked(async () => {
await this.loadAffected();
});
});
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

private async loadAffected() {
const feedbackDetail = this.feedbackDetail();
const pageable: PageableSearch = {
page: this.page(),
pageSize: this.pageSize(),
sortedColumn: 'participationId',
sortingOrder: SortingOrder.ASCENDING,
};

try {
const response = await this.feedbackService.getParticipationForFeedbackIds(this.exerciseId(), feedbackDetail.concatenatedFeedbackIds, pageable);
this.participation.set(response);
} catch (error) {
this.alertService.error('There was an error while loading the affected Students for this feedback');
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

setPage(newPage: number): void {
this.page.set(newPage);
this.loadAffected();
}
}
Loading
Loading