Skip to content

Commit

Permalink
implementation first draft with levenshtein
Browse files Browse the repository at this point in the history
  • Loading branch information
az108 committed Nov 30, 2024
1 parent dbcf03d commit 0ea5d26
Show file tree
Hide file tree
Showing 18 changed files with 163 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) {
* Pagination and sorting:
* - Sorting is applied based on the specified column and order (ascending or descending).
* - The result is paginated according to the provided page number and page size.
* Additionally one can group by the Levenshtein distance of the feedback detail text.
*
* @param exerciseId The ID of the exercise for which feedback details should be retrieved.
* @param data The {@link FeedbackPageableDTO} containing page number, page size, search term, sorting options, and filtering parameters
Expand All @@ -584,7 +585,7 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) {
* - A list of active test case names used in the feedback.
* - A list of predefined error categories ("Student Error," "Ares Error," "AST Error") available for filtering.
*/
public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, FeedbackPageableDTO data, String levinStein) {
public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, FeedbackPageableDTO data, String levenshtein) {

// 1. Fetch programming exercise with associated test cases
ProgrammingExercise programmingExercise = programmingExerciseRepository.findWithTestCasesByIdElseThrow(exerciseId);
Expand Down Expand Up @@ -619,11 +620,11 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee
// 8. Set up pagination and sorting based on input data
final var pageable = PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.FEEDBACK_ANALYSIS);

// 9. Query the database to retrieve paginated and filtered feedback
// 9. Query the database based on levenshtein attribute to retrieve paginated and filtered feedback
final Page<FeedbackDetailDTO> feedbackDetailPage;
List<FeedbackDetailDTO> processedDetails;
int totalPages = 0;
if (levinStein.equals("false")) {
if (levenshtein.equals("false")) {
feedbackDetailPage = studentParticipationRepository.findFilteredFeedbackByExerciseId(exerciseId,
StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), includeUnassignedTasks, minOccurrence,
maxOccurrence, filterErrorCategories, pageable);
Expand All @@ -641,8 +642,8 @@ public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, Fee
// Fetch all feedback details
List<FeedbackDetailDTO> allFeedbackDetails = feedbackDetailPage.getContent();

// Apply Levenshtein-based grouping and aggregation
List<FeedbackDetailDTO> aggregatedFeedbackDetails = aggregateUsingLevenshtein(allFeedbackDetails, 0.5);
// Apply Levenshtein-based grouping and aggregation with a similarity threshold of 90%
List<FeedbackDetailDTO> aggregatedFeedbackDetails = aggregateUsingLevenshtein(allFeedbackDetails, 0.9);

// Apply manual pagination
int page = data.getPage();
Expand Down Expand Up @@ -688,7 +689,7 @@ private List<FeedbackDetailDTO> aggregateUsingLevenshtein(List<FeedbackDetailDTO

FeedbackDetailDTO base = feedbackDetails.get(i);
List<String> aggregatedTexts = new ArrayList<>();
aggregatedTexts.add(base.detailText().getFirst()); // Add the primary text
aggregatedTexts.add(base.detailText().getFirst());
long totalCount = base.count();

for (int j = i + 1; j < feedbackDetails.size(); j++) {
Expand Down Expand Up @@ -745,7 +746,7 @@ public long getMaxCountForExercise(long exerciseId) {
* @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> getAffectedStudentsWithFeedbackText(long exerciseId, String detailText, String testCaseName, PageableSearchDTO<String> data) {
public Page<FeedbackAffectedStudentDTO> getAffectedStudentsWithFeedbackText(long exerciseId, List<String> detailText, String testCaseName, PageableSearchDTO<String> data) {
PageRequest pageRequest = PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.AFFECTED_STUDENTS);
return studentParticipationRepository.findAffectedStudentsByFeedbackText(exerciseId, detailText, testCaseName, pageRequest);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -309,18 +311,19 @@ public ResponseEntity<Result> createResultForExternalSubmission(@PathVariable Lo
* </li>
* </ul>
*
* @param exerciseId The unique identifier of the exercise for which feedback details are requested.
* @param data A {@link FeedbackPageableDTO} object containing pagination, sorting, and filtering parameters, including:
* <ul>
* <li>Page number and page size</li>
* <li>Search term (optional)</li>
* <li>Sorting order (ASCENDING or DESCENDING)</li>
* <li>Sorted column</li>
* <li>Filter task names (optional)</li>
* <li>Filter test case names (optional)</li>
* <li>Occurrence range (optional)</li>
* <li>Error categories (optional)</li>
* </ul>
* @param exerciseId The unique identifier of the exercise for which feedback details are requested.
* @param levenshtein Should the feedback be grouped via Levenshtein distance.
* @param data A {@link FeedbackPageableDTO} object containing pagination, sorting, and filtering parameters, including:
* <ul>
* <li>Page number and page size</li>
* <li>Search term (optional)</li>
* <li>Sorting order (ASCENDING or DESCENDING)</li>
* <li>Sorted column</li>
* <li>Filter task names (optional)</li>
* <li>Filter test case names (optional)</li>
* <li>Occurrence range (optional)</li>
* <li>Error categories (optional)</li>
* </ul>
* @return A {@link ResponseEntity} containing a {@link FeedbackAnalysisResponseDTO}, which includes:
* <ul>
* <li>{@link SearchResultPageDTO < FeedbackDetailDTO >} feedbackDetails: Paginated and filtered feedback details for the exercise.</li>
Expand All @@ -332,9 +335,9 @@ public ResponseEntity<Result> createResultForExternalSubmission(@PathVariable Lo
*/
@GetMapping("exercises/{exerciseId}/feedback-details")
@EnforceAtLeastEditorInExercise
public ResponseEntity<FeedbackAnalysisResponseDTO> getFeedbackDetailsPaged(@PathVariable long exerciseId, @RequestParam("levinStein") String levinStein,
public ResponseEntity<FeedbackAnalysisResponseDTO> getFeedbackDetailsPaged(@PathVariable long exerciseId, @RequestParam("levenshtein") String levenshtein,
@ModelAttribute FeedbackPageableDTO data) {
FeedbackAnalysisResponseDTO response = resultService.getFeedbackDetailsOnPage(exerciseId, data, levinStein);
FeedbackAnalysisResponseDTO response = resultService.getFeedbackDetailsOnPage(exerciseId, data, levenshtein);
return ResponseEntity.ok(response);
}

Expand All @@ -359,15 +362,26 @@ public ResponseEntity<Long> getMaxCount(@PathVariable long exerciseId) {
* and participation details.
* <br>
*
* @param exerciseId for which the participation data is requested.
* @param detailText to filter affected students by specific feedback entries.
* @param data A {@link PageableSearchDTO} object containing pagination and sorting parameters.
* @param exerciseId for which the participation data is requested.
* @param detailText1 Optional first detail text to filter affected students by specific feedback entries.
* @param detailText2 Optional second detail text to filter affected students by specific feedback entries.
* @param detailText3 Optional third detail text to filter affected students by specific feedback entries.
* @param detailText4 Optional fourth detail text to filter affected students by specific feedback entries.
* @param detailText5 Optional fifth detail text to filter affected students by specific feedback entries.
* @param testCaseName The test case name to filter affected students by specific test cases.
* @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, @RequestParam("detailText") String detailText,
@RequestParam("testCaseName") String testCaseName, @ModelAttribute PageableSearchDTO<String> data) {
public ResponseEntity<Page<FeedbackAffectedStudentDTO>> getAffectedStudentsWithFeedback(@PathVariable long exerciseId,
@RequestParam(value = "detailText1", required = false) String detailText1, @RequestParam(value = "detailText2", required = false) String detailText2,
@RequestParam(value = "detailText3", required = false) String detailText3, @RequestParam(value = "detailText4", required = false) String detailText4,
@RequestParam(value = "detailText5", required = false) String detailText5, @RequestParam("testCaseName") String testCaseName,
@ModelAttribute PageableSearchDTO<String> data) {

List<String> detailText = Stream.of(detailText1, detailText2, detailText3, detailText4, detailText5).filter(Objects::nonNull).toList();

Page<FeedbackAffectedStudentDTO> participation = resultService.getAffectedStudentsWithFeedbackText(exerciseId, detailText, testCaseName, data);
return ResponseEntity.ok(participation);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,7 @@ SELECT MAX(pr.id)
* @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(
SELECT DISTINCT new de.tum.cit.aet.artemis.assessment.dto.FeedbackAffectedStudentDTO(
p.exercise.course.id,
p.id,
p.student.firstName,
Expand All @@ -1366,12 +1366,12 @@ SELECT MAX(pr.id)
)
INNER JOIN r.feedbacks f
WHERE p.exercise.id = :exerciseId
AND f.detailText = :detailText
AND f.detailText IN :detailText
AND f.testCase.testName = :testCaseName
AND p.testRun = FALSE
ORDER BY p.student.firstName ASC
""")
Page<FeedbackAffectedStudentDTO> findAffectedStudentsByFeedbackText(@Param("exerciseId") long exerciseId, @Param("detailText") String detailText,
Page<FeedbackAffectedStudentDTO> findAffectedStudentsByFeedbackText(@Param("exerciseId") long exerciseId, @Param("detailText") List<String> detailText,
@Param("testCaseName") String testCaseName, Pageable pageable);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ <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">
@if (levenshtein()) {
<div class="alert alert-info mb-3">
<p>
<strong [jhiTranslate]="TRANSLATION_BASE + '.levinsteinInfoTitle'"></strong>
<span [jhiTranslate]="TRANSLATION_BASE + '.levinsteinInfoDescription'"></span>
</p>
</div>
}
<table class="table table-striped mb-3">
<colgroup>
<col class="col" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { PageableResult, PageableSearch, SortingOrder } from 'app/shared/table/p
export class AffectedStudentsModalComponent {
exerciseId = input.required<number>();
feedbackDetail = input.required<FeedbackDetail>();
levenshtein = input.required<boolean>();
readonly participation = signal<PageableResult<FeedbackAffectedStudentDTO>>({ content: [], totalPages: 0, totalElements: 0 });
readonly TRANSLATION_BASE = 'artemisApp.programmingExercise.configureGrading.feedbackAnalysis.affectedStudentsModal';

Expand Down Expand Up @@ -45,12 +46,7 @@ export class AffectedStudentsModalComponent {
};

try {
const response = await this.feedbackService.getParticipationForFeedbackDetailText(
this.exerciseId(),
feedbackDetail.detailText[0],
feedbackDetail.testCaseName,
pageable,
);
const response = await this.feedbackService.getParticipationForFeedbackDetailText(this.exerciseId(), feedbackDetail.detailText, feedbackDetail.testCaseName, pageable);
this.participation.set(response);
} catch (error) {
this.alertService.error(this.TRANSLATION_BASE + '.error');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ <h4 class="modal-title">
<label for="regular" class="btn btn-outline-secondary" [jhiTranslate]="TRANSLATION_BASE + '.announcementChannelNo'"></label>
</div>
</div>
@if (levenshtein()) {
<div class="alert alert-info mb-3">
<p>
<strong [jhiTranslate]="TRANSLATION_BASE + '.levinsteinInfoTitle'"></strong>
<span [jhiTranslate]="TRANSLATION_BASE + '.levinsteinInfoDescription'"></span>
</p>
</div>
}
<div class="form-group mt-3">
<p class="text-info">
<strong [jhiTranslate]="TRANSLATION_BASE + '.studentNumber'" [translateValues]="{ count: feedbackDetail().count }"></strong>
Expand All @@ -81,7 +89,7 @@ <h4 class="modal-title">
[disabled]="!form.valid"
[jhiTranslate]="TRANSLATION_BASE + '.createAndNavigateLabel'"
></button>
<button (click)="submitForm(false)" type="button" class="btn btn-primary" [disabled]="!form.valid" [jhiTranslate]="TRANSLATION_BASE + '.createChannelButton'"></button>
<button (click)="submitForm(false)" type="button" class="btn btn-primary" [disabled]="!form.valid" [jhiTranslate]="TRANSLATION_BASE + '.createChannelLabel'"></button>
</div>
</form>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { AlertService } from 'app/core/util/alert.service';
export class FeedbackDetailChannelModalComponent {
protected readonly TRANSLATION_BASE = 'artemisApp.programmingExercise.configureGrading.feedbackAnalysis.feedbackDetailChannel';
feedbackDetail = input.required<FeedbackDetail>();
levenshtein = input.required<boolean>();
formSubmitted = output<{ channelDto: ChannelDTO; navigate: boolean }>();

isConfirmModalOpen = signal(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,10 @@
<h3 class="m-0 p-0" [jhiTranslate]="TRANSLATION_BASE + '.title'" [translateValues]="{ exerciseTitle: exerciseTitle() }"></h3>
<div class="d-flex align-items-center">
<div class="form-switch d-flex align-items-center flex-md-shrink-0 me-2">
<input
class="form-check-input mt-0"
type="checkbox"
(click)="toggleLevinStein()"
[checked]="levinStein()"
aria-label="TRANSLATION_BASE + '.levinsteinToggle'"
style="width: 2.5rem; height: 1.25rem"
/>
<input class="form-check-input mt-0" type="checkbox" (click)="toggleLevenshtein()" [checked]="levenshtein()" style="width: 2.5rem; height: 1.25rem" />
<label class="ms-1 me-2 mb-0">
<span>Group Feedback</span>
<fa-icon size="sm" [icon]="faCircleInfo" ngbTooltip="Placeholder" class="text-info"></fa-icon>
<span [jhiTranslate]="TRANSLATION_BASE + '.groupFeedback'"></span>
<fa-icon size="sm" [icon]="faCircleInfo" [ngbTooltip]="TRANSLATION_BASE + '.groupFeedbackTooltip' | artemisTranslate" class="text-info"></fa-icon>
</label>
</div>
<button class="btn me-2" (click)="openFilterModal()" [ngClass]="{ 'btn-secondary': selectedFiltersCount() == 0, 'btn-success': selectedFiltersCount() != 0 }">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class FeedbackAnalysisComponent {
private isFeedbackDetailChannelModalOpen = false;

private readonly debounceLoadData = BaseApiHttpService.debounce(this.loadData.bind(this), 300);
readonly levinStein = signal<boolean>(false);
readonly levenshtein = signal<boolean>(false);

constructor() {
effect(() => {
Expand All @@ -98,7 +98,7 @@ export class FeedbackAnalysisComponent {
};

try {
const response = await this.feedbackAnalysisService.search(state, this.levinStein(), {
const response = await this.feedbackAnalysisService.search(state, this.levenshtein(), {
exerciseId: this.exerciseId(),
filters: {
tasks: this.selectedFiltersCount() !== 0 ? savedTasks : [],
Expand Down Expand Up @@ -202,6 +202,7 @@ export class FeedbackAnalysisComponent {
const modalRef = this.modalService.open(AffectedStudentsModalComponent, { centered: true, size: 'lg' });
modalRef.componentInstance.exerciseId = this.exerciseId;
modalRef.componentInstance.feedbackDetail = signal(feedbackDetail);
modalRef.componentInstance.levenshtein = signal(this.levenshtein());
}

async openFeedbackDetailChannelModal(feedbackDetail: FeedbackDetail): Promise<void> {
Expand All @@ -211,6 +212,7 @@ export class FeedbackAnalysisComponent {
this.isFeedbackDetailChannelModalOpen = true;
const modalRef = this.modalService.open(FeedbackDetailChannelModalComponent, { centered: true, size: 'lg' });
modalRef.componentInstance.feedbackDetail = signal(feedbackDetail);
modalRef.componentInstance.levenshtein = signal(this.levenshtein());
modalRef.componentInstance.formSubmitted.subscribe(async ({ channelDto, navigate }: { channelDto: ChannelDTO; navigate: boolean }) => {
try {
const feedbackChannelRequest: FeedbackChannelRequestDTO = {
Expand Down Expand Up @@ -240,8 +242,8 @@ export class FeedbackAnalysisComponent {
}
}

toggleLevinStein(): void {
this.levinStein.update((current) => !current);
toggleLevenshtein(): void {
this.levenshtein.update((current) => !current);
this.loadData();
}
}
Loading

0 comments on commit 0ea5d26

Please sign in to comment.