From cd724134e3aac35ac05eab8ac602ef0c380311ce Mon Sep 17 00:00:00 2001 From: = Enea_Gore Date: Thu, 12 Sep 2024 18:28:34 +0200 Subject: [PATCH 01/50] Add new router link to pick submission, improve error handling and ui, add text blocks --- .../exercise/service/SubmissionService.java | 1 + .../exercise/web/ParticipationResource.java | 2 +- .../service/TextExerciseFeedbackService.java | 72 ++++++--- .../text/web/TextExerciseResource.java | 75 +++++++++ .../header-participation-page.component.html | 2 +- .../header-participation-page.component.ts | 2 +- .../shared/feedback/feedback.component.html | 11 ++ .../shared/feedback/feedback.component.ts | 3 +- .../shared/result/result.component.html | 2 +- .../shared/result/result.component.ts | 19 ++- .../exercises/shared/result/result.service.ts | 3 + .../exercises/shared/result/result.utils.ts | 18 ++- .../result/updating-result.component.ts | 1 + .../participate/text-editor.component.html | 88 ++++++++-- .../participate/text-editor.component.scss | 26 +++ .../text/participate/text-editor.component.ts | 153 +++++++++++++++++- .../text/participate/text-editor.route.ts | 9 ++ .../text/participate/text-editor.service.ts | 5 + .../course-exercise-details.component.ts | 11 +- ...ise-details-student-actions.component.html | 16 +- ...rcise-details-student-actions.component.ts | 4 +- .../result-history.component.html | 16 +- .../result-history.component.ts | 77 ++++++++- .../result-history/result-history.scss | 4 + .../submission-result-status.component.html | 1 + .../submission-result-status.component.ts | 1 + src/main/webapp/i18n/de/global.json | 1 + src/main/webapp/i18n/en/global.json | 1 + 28 files changed, 556 insertions(+), 68 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/service/SubmissionService.java b/src/main/java/de/tum/cit/aet/artemis/exercise/service/SubmissionService.java index fc72035f5f70..2669e856337f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/service/SubmissionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/service/SubmissionService.java @@ -787,6 +787,7 @@ private List getSubmissionsWithComplaintsFromComplai // add each submission with its complaint to the DTO submissions.stream().filter(submission -> submission.getResultWithComplaint() != null).forEach(submission -> { // get the complaint which belongs to the submission + submission.setResults(submission.getNonAthenaResults()); Complaint complaintOfSubmission = complaintMap.get(submission.getResultWithComplaint().getId()); prepareComplaintAndSubmission(complaintOfSubmission, submission); submissionWithComplaintDTOs.add(new SubmissionWithComplaintDTO(submission, complaintOfSubmission)); diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index a9f3d94ae2d2..6e84cfec8d03 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -414,7 +414,7 @@ else if (exercise instanceof ProgrammingExercise) { // Process feedback request StudentParticipation updatedParticipation; if (exercise instanceof TextExercise) { - updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(exercise.getId(), participation, (TextExercise) exercise); + updatedParticipation = textExerciseFeedbackService.handleNonGradedFeedbackRequest(participation, (TextExercise) exercise); } else { updatedParticipation = programmingExerciseCodeReviewFeedbackService.handleNonGradedFeedbackRequest(exercise.getId(), diff --git a/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java index 69f345c535cd..490a38fcda21 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/service/TextExerciseFeedbackService.java @@ -3,9 +3,14 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,11 +26,10 @@ import de.tum.cit.aet.artemis.assessment.web.ResultWebsocketService; import de.tum.cit.aet.artemis.athena.service.AthenaFeedbackSuggestionsService; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; -import de.tum.cit.aet.artemis.core.exception.InternalServerErrorException; -import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.service.ParticipationService; import de.tum.cit.aet.artemis.exercise.service.SubmissionService; +import de.tum.cit.aet.artemis.text.domain.TextBlock; import de.tum.cit.aet.artemis.text.domain.TextExercise; import de.tum.cit.aet.artemis.text.domain.TextSubmission; @@ -49,14 +53,18 @@ public class TextExerciseFeedbackService { private final ResultRepository resultRepository; + private final TextBlockService textBlockService; + public TextExerciseFeedbackService(Optional athenaFeedbackSuggestionsService, SubmissionService submissionService, - ResultService resultService, ResultRepository resultRepository, ResultWebsocketService resultWebsocketService, ParticipationService participationService) { + ResultService resultService, ResultRepository resultRepository, ResultWebsocketService resultWebsocketService, ParticipationService participationService, + TextBlockService textBlockService) { this.athenaFeedbackSuggestionsService = athenaFeedbackSuggestionsService; this.submissionService = submissionService; this.resultService = resultService; this.resultRepository = resultRepository; this.resultWebsocketService = resultWebsocketService; this.participationService = participationService; + this.textBlockService = textBlockService; } private void checkRateLimitOrThrow(StudentParticipation participation) { @@ -74,12 +82,11 @@ private void checkRateLimitOrThrow(StudentParticipation participation) { * Handles the request for generating feedback for a text exercise. * Unlike programming exercises a tutor is not notified if Athena is not available. * - * @param exerciseId the id of the text exercise. * @param participation the student participation associated with the exercise. * @param textExercise the text exercise object. * @return StudentParticipation updated text exercise for an AI assessment */ - public StudentParticipation handleNonGradedFeedbackRequest(Long exerciseId, StudentParticipation participation, TextExercise textExercise) { + public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation participation, TextExercise textExercise) { if (this.athenaFeedbackSuggestionsService.isPresent()) { this.checkRateLimitOrThrow(participation); CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, textExercise)); @@ -103,50 +110,79 @@ public void generateAutomaticNonGradedFeedback(StudentParticipation participatio if (submissionOptional.isEmpty()) { throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); } - var submission = submissionOptional.get(); + TextSubmission textSubmission = (TextSubmission) submissionOptional.get(); + + if (textSubmission.getNonAthenaResults().stream().filter(Objects::nonNull).collect(Collectors.toCollection(ArrayList::new)).isEmpty()) { + throw new BadRequestAlertException("Submission already has an athena result", "submission", "hasAthenaFeedback"); + } Result automaticResult = new Result(); automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); automaticResult.setRated(true); automaticResult.setScore(0.0); automaticResult.setSuccessful(null); - automaticResult.setSubmission(submission); + automaticResult.setSubmission(textSubmission); automaticResult.setParticipation(participation); try { - this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult); + this.resultWebsocketService.broadcastNewResult(participation, automaticResult); - log.debug("Submission id: {}", submission.getId()); + log.debug("Submission id: {}", textSubmission.getId()); - var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, (TextSubmission) submission, false); + var athenaResponse = this.athenaFeedbackSuggestionsService.orElseThrow().getTextFeedbackSuggestions(textExercise, textSubmission, true); - List feedbacks = athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).map(individualFeedbackItem -> { + Set textBlocks = new HashSet<>(); + List feedbacks = new ArrayList<>(); + + athenaResponse.stream().filter(individualFeedbackItem -> individualFeedbackItem.description() != null).forEach(individualFeedbackItem -> { + var textBlock = new TextBlock(); var feedback = new Feedback(); + feedback.setText(individualFeedbackItem.title()); feedback.setDetailText(individualFeedbackItem.description()); feedback.setHasLongFeedbackText(false); feedback.setType(FeedbackType.AUTOMATIC); feedback.setCredits(individualFeedbackItem.credits()); - return feedback; - }).toList(); + + textBlock.setStartIndex(individualFeedbackItem.indexStart()); + textBlock.setEndIndex(individualFeedbackItem.indexEnd()); + textBlock.setSubmission(textSubmission); + textBlock.setTextFromSubmission(); + textBlock.automatic(); + textBlock.computeId(); + feedback.setReference(textBlock.getId()); + textBlock.setFeedback(feedback); + log.debug(textBlock.toString()); + + textBlocks.add(textBlock); + feedbacks.add(feedback); + }); double totalFeedbacksScore = 0.0; for (Feedback feedback : feedbacks) { totalFeedbacksScore += feedback.getCredits(); } totalFeedbacksScore = totalFeedbacksScore / textExercise.getMaxPoints() * 100; - automaticResult.setSuccessful(true); automaticResult.setCompletionDate(ZonedDateTime.now()); - automaticResult.setScore(Math.clamp(totalFeedbacksScore, 0, 100)); + // For Athena automatic results successful = true will mean that the generation was successful + // undefined in progress and false it failed + automaticResult.setSuccessful(true); + automaticResult = this.resultRepository.save(automaticResult); resultService.storeFeedbackInResult(automaticResult, feedbacks, true); - submissionService.saveNewResult(submission, automaticResult); - this.resultWebsocketService.broadcastNewResult((Participation) participation, automaticResult); + textBlockService.saveAll(textBlocks); + textSubmission.setBlocks(textBlocks); + submissionService.saveNewResult(textSubmission, automaticResult); + this.resultWebsocketService.broadcastNewResult(participation, automaticResult); } catch (Exception e) { log.error("Could not generate feedback", e); - throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated"); + // Broadcast the failed result but don't save, note that successful = false is normally used to indicate a score < 100 + // but since we do not differentiate for athena feedback we use it to indicate a failed generation + automaticResult.setSuccessful(false); + automaticResult.setCompletionDate(null); + this.resultWebsocketService.broadcastNewResult(participation, automaticResult); } } } diff --git a/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java index 8f98cd911c51..4c779752ecb5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/web/TextExerciseResource.java @@ -465,6 +465,81 @@ public ResponseEntity getDataForTextEditor(@PathVariable L return ResponseEntity.ok(participation); } + @GetMapping("text-editor-all/{participationId}") + @EnforceAtLeastStudent + public ResponseEntity getDataForTextEditorWithAllSubmissionsAndResults(@PathVariable Long participationId) { + User user = userRepository.getUserWithGroupsAndAuthorities(); + StudentParticipation participation = studentParticipationRepository.findByIdWithLegalSubmissionsResultsFeedbackElseThrow(participationId); + if (!(participation.getExercise() instanceof TextExercise textExercise)) { + throw new BadRequestAlertException("The exercise of the participation is not a text exercise.", ENTITY_NAME, "wrongExerciseType"); + } + + // users can only see their own submission (to prevent cheating), TAs, instructors and admins can see all answers + if (!authCheckService.isOwnerOfParticipation(participation, user) && !authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) { + throw new AccessForbiddenException(); + } + + // Exam exercises cannot be seen by students between the endDate and the publishResultDate + if (!authCheckService.isAllowedToGetExamResult(textExercise, participation, user)) { + throw new AccessForbiddenException(); + } + + // if no results, check if there are really no results or the relation to results was not updated yet + if (participation.getResults().isEmpty()) { + List results = resultRepository.findByParticipationIdOrderByCompletionDateDesc(participation.getId()); + participation.setResults(new HashSet<>(results)); + } + + Set submissions = participation.getSubmissions(); + participation.setSubmissions(new HashSet<>()); + for (Submission submission : submissions) { + if (submission != null) { + TextSubmission textSubmission = (TextSubmission) submission; + + // set reference to participation to null, since we are already inside a participation + textSubmission.setParticipation(null); + + if (!ExerciseDateService.isAfterAssessmentDueDate(textExercise)) { + // We want to have the preliminary feedback before the assessment due date too + List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); + textSubmission.setResults(athenaResults); + } + + Result result = textSubmission.getLatestResult(); + if (result != null) { + // Load TextBlocks for the Submission. They are needed to display the Feedback in the client. + final var textBlocks = textBlockRepository.findAllBySubmissionId(textSubmission.getId()); + textSubmission.setBlocks(textBlocks); + + if (textSubmission.isSubmitted() && result.getCompletionDate() != null) { + List assessments = feedbackRepository.findByResult(result); + result.setFeedbacks(assessments); + } + + if (!authCheckService.isAtLeastTeachingAssistantForExercise(textExercise, user)) { + result.filterSensitiveInformation(); + } + + // only send the one latest result to the client + textSubmission.setResults(List.of(result)); + // participation.setResults(Set.of(result)); + participation.addResult(result); + } + + participation.addSubmission(textSubmission); + } + if (!(authCheckService.isAtLeastInstructorForExercise(textExercise, user) || participation.isOwnedBy(user))) { + participation.filterSensitiveInformation(); + } + + textExercise.filterSensitiveInformation(); + if (textExercise.isExamExercise()) { + textExercise.getExam().setCourse(null); + } + } + return ResponseEntity.ok(participation); + } + /** * Search for all text exercises by id, title and course title. The result is pageable since there * might be hundreds of exercises in the DB. diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.html index af31df28d183..dcf03cb52d86 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.html @@ -63,7 +63,7 @@
{{ 'artemisApp.exercise.assessmentDueDate' | artemisTransl
@if (participation && resultsPublished) {
- +
}
diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts index da7c2b549735..0fb261ec3ea8 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-participation-page.component.ts @@ -19,7 +19,7 @@ export class HeaderParticipationPageComponent implements OnInit, OnChanges { @Input() title: string; @Input() exercise: Exercise; @Input() participation: StudentParticipation; - + @Input() isInsideEditor = false; public exerciseStatusBadge = 'bg-success'; public exerciseCategories: ExerciseCategory[]; public achievedPoints?: number; diff --git a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html index a0de0676f7dc..e701f4cde36b 100644 --- a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html +++ b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html @@ -202,6 +202,17 @@