From ab5af3c160690da5c00d9532bffe6815a319dd45 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Tue, 17 Sep 2024 03:04:24 +0200 Subject: [PATCH 01/28] move request feedback to a standalone component and add a button to the repository view --- .../code-editor-actions.component.html | 3 + .../actions/code-editor-actions.component.ts | 4 +- .../shared/code-editor/code-editor.module.ts | 2 + .../code-editor-container.component.html | 1 + .../exercise-buttons.module.ts | 3 +- ...ise-details-student-actions.component.html | 45 ++---- ...rcise-details-student-actions.component.ts | 97 +---------- .../request-feedback-button.component.html | 37 +++++ .../request-feedback-button.component.ts | 153 ++++++++++++++++++ src/main/webapp/i18n/de/exercise.json | 2 +- src/main/webapp/i18n/en/exercise.json | 2 +- 11 files changed, 213 insertions(+), 136 deletions(-) create mode 100644 src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html create mode 100644 src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html b/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html index 29e3ded8363c..7737158d8ce5 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html @@ -1,3 +1,6 @@ +@if (!!participation()?.exercise) { + +} @if (commitState === CommitState.CONFLICT) {
diff --git a/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts b/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts index 1b36ab7e6f5d..7b6aab3d5f7c 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts @@ -6,9 +6,10 @@ import { OrionExerciseDetailsStudentActionsComponent } from 'app/orion/participa import { ExerciseDetailsStudentActionsComponent } from 'app/overview/exercise-details/exercise-details-student-actions.component'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedPipesModule } from 'app/shared/pipes/shared-pipes.module'; +import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component'; @NgModule({ - imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisSharedPipesModule, OrionModule, FeatureToggleModule], + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisSharedPipesModule, OrionModule, FeatureToggleModule, RequestFeedbackButtonComponent], declarations: [ExerciseDetailsStudentActionsComponent, OrionExerciseDetailsStudentActionsComponent], exports: [ExerciseDetailsStudentActionsComponent, OrionExerciseDetailsStudentActionsComponent], }) diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html index 148fdfdef932..b38b3bbcda0c 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html @@ -135,30 +135,8 @@ } - @if (exercise.allowFeedbackRequests) { - @if (athenaEnabled) { - - - Send automatic feedback request - - } @else { - - - Send manual feedback request - - } + @if (exercise.allowFeedbackRequests && gradedParticipation) { + } } @@ -251,18 +229,13 @@ [hideLabelMobile]="false" [routerLink]="['/courses', courseId, 'exercises', exercise.type + '-exercises', exercise.id, 'participate', gradedParticipation!.id]" > - @if (exercise.allowFeedbackRequests && athenaEnabled && exercise.type === ExerciseType.TEXT) { - + @if (exercise.allowFeedbackRequests && gradedParticipation) { + } } diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts index 1e70f2cfc3ee..b3ac7a93e8e7 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts @@ -18,7 +18,7 @@ import { ParticipationService } from 'app/exercises/shared/participation/partici import dayjs from 'dayjs/esm'; import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; -import { PROFILE_ATHENA, PROFILE_LOCALVC, PROFILE_THEIA } from 'app/app.constants'; +import { PROFILE_LOCALVC, PROFILE_THEIA } from 'app/app.constants'; import { AssessmentType } from 'app/entities/assessment-type.model'; @Component({ @@ -40,7 +40,6 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges @Input() smallButtons: boolean; @Input() examMode: boolean; @Input() isGeneratingFeedback: boolean; - @Output() generatingFeedback: EventEmitter = new EventEmitter(); // extension points, see shared/extension-point @@ -56,7 +55,6 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges beforeDueDate: boolean; editorLabel?: string; localVCEnabled = false; - athenaEnabled = false; routerLink: string; repositoryLink: string; @@ -73,8 +71,6 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges readonly faDesktop = faDesktop; readonly faPenSquare = faPenSquare; - private feedbackSent = false; - constructor( private alertService: AlertService, private courseExerciseService: CourseExerciseService, @@ -109,7 +105,6 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges this.programmingExercise = this.exercise as ProgrammingExercise; this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles?.includes(PROFILE_LOCALVC); - this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); // The online IDE is only available with correct SpringProfile and if it's enabled for this exercise if (profileInfo.activeProfiles?.includes(PROFILE_THEIA) && this.programmingExercise) { @@ -138,9 +133,6 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges this.editorLabel = 'openModelingEditor'; } else if (this.exercise.type === ExerciseType.TEXT) { this.editorLabel = 'openTextEditor'; - this.profileService.getProfileInfo().subscribe((profileInfo) => { - this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); - }); } else if (this.exercise.type === ExerciseType.FILE_UPLOAD) { this.editorLabel = 'uploadFile'; } @@ -255,29 +247,6 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges }); } - requestFeedback() { - if (!this.assureConditionsSatisfied()) return; - if (this.exercise.type === ExerciseType.PROGRAMMING) { - const confirmLockRepository = this.translateService.instant('artemisApp.exercise.lockRepositoryWarning'); - if (!window.confirm(confirmLockRepository)) { - return; - } - } - - this.courseExerciseService.requestFeedback(this.exercise.id!).subscribe({ - next: (participation: StudentParticipation) => { - if (participation) { - this.generatingFeedback.emit(); - this.feedbackSent = true; - this.alertService.success('artemisApp.exercise.feedbackRequestSent'); - } - }, - error: (error) => { - this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); - }, - }); - } - get isBeforeStartDateAndStudent(): boolean { return !this.exercise.isAtLeastTutor && !!this.exercise.startDate && dayjs().isBefore(this.exercise.startDate); } @@ -330,68 +299,4 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges buildPlanUrl(participation: StudentParticipation) { return (participation as ProgrammingExerciseStudentParticipation).buildPlanUrl; } - - /** - * Checks if the conditions for requesting automatic non-graded feedback are satisfied. - * The student can request automatic non-graded feedback under the following conditions: - * 1. They have a graded submission. - * 2. The deadline for the exercise has not been exceeded. - * 3. There is no already pending feedback request. - * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. - */ - assureConditionsSatisfied(): boolean { - this.updateParticipations(); - if (this.exercise.type === ExerciseType.PROGRAMMING) { - const latestResult = this.gradedParticipation?.results && this.gradedParticipation.results.find(({ assessmentType }) => assessmentType === AssessmentType.AUTOMATIC); - const someHiddenTestsPassed = latestResult?.score !== undefined; - const testsNotPassedWarning = this.translateService.instant('artemisApp.exercise.notEnoughPoints'); - if (!someHiddenTestsPassed) { - window.alert(testsNotPassedWarning); - return false; - } - } - - const afterDueDate = !this.exercise.dueDate || dayjs().isSameOrAfter(this.exercise.dueDate); - const dueDateWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAfterDueDate'); - if (afterDueDate) { - this.alertService.warning(dueDateWarning); - return false; - } - - const requestAlreadySent = (this.gradedParticipation?.individualDueDate && this.gradedParticipation.individualDueDate.isBefore(Date.now())) ?? false; - const requestAlreadySentWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAlreadySent'); - if (requestAlreadySent) { - this.alertService.warning(requestAlreadySentWarning); - return false; - } - - if (this.gradedParticipation?.results) { - const athenaResults = this.gradedParticipation.results.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA'); - const countOfSuccessfulRequests = athenaResults.length; - - if (countOfSuccessfulRequests >= 10) { - const rateLimitExceededWarning = this.translateService.instant('artemisApp.exercise.maxAthenaResultsReached'); - this.alertService.warning(rateLimitExceededWarning); - return false; - } - } - - if (this.hasAthenaResultForlatestSubmission()) { - const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); - this.alertService.warning(submitFirstWarning); - return false; - } - return true; - } - - hasAthenaResultForlatestSubmission(): boolean { - if (this.gradedParticipation?.submissions && this.gradedParticipation?.results) { - // submissions.results is always undefined so this is neccessary - return ( - this.gradedParticipation.submissions.last()?.id === - this.gradedParticipation?.results.filter((result) => result.assessmentType == AssessmentType.AUTOMATIC_ATHENA).first()?.submission?.id - ); - } - return false; - } } diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html new file mode 100644 index 000000000000..b5f24d538924 --- /dev/null +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html @@ -0,0 +1,37 @@ +@if (athenaEnabled && !isExamExercise) { + @if (exercise().type === ExerciseType.TEXT) { + + } @else { + + + + + } +} @else { + + + + +} diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts new file mode 100644 index 000000000000..5c40f24c8189 --- /dev/null +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -0,0 +1,153 @@ +import { Component, input, inject, OnInit, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faPenSquare } from '@fortawesome/free-solid-svg-icons'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { PROFILE_ATHENA } from 'app/app.constants'; +import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { AlertService } from 'app/core/util/alert.service'; +import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; +import { AssessmentType } from 'app/entities/assessment-type.model'; +import { TranslateService } from '@ngx-translate/core'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import dayjs from 'dayjs/esm'; +import { isExamExercise } from 'app/shared/util/utils'; +import { ExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { HttpResponse } from '@angular/common/http'; +import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; + +@Component({ + selector: 'jhi-request-feedback-button', + standalone: true, + imports: [CommonModule, ArtemisSharedCommonModule, NgbTooltipModule, FontAwesomeModule], + templateUrl: './request-feedback-button.component.html', +}) +export class RequestFeedbackButtonComponent implements OnInit { + faPenSquare = faPenSquare; + athenaEnabled = false; + isExamExercise: boolean; + participation?: StudentParticipation; + + isGeneratingFeedback = input(); + smallButtons = input(false); + exercise = input.required(); + generatingFeedback = output(); + + private feedbackSent = false; + private profileService = inject(ProfileService); + private alertService = inject(AlertService); + private courseExerciseService = inject(CourseExerciseService); + private translateService = inject(TranslateService); + private exerciseService = inject(ExerciseService); + private participationService = inject(ParticipationService); + + protected readonly ExerciseType = ExerciseType; + + ngOnInit() { + this.profileService.getProfileInfo().subscribe((profileInfo) => { + this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); + }); + this.isExamExercise = isExamExercise(this.exercise()); + if (this.isExamExercise || !this.exercise().id) { + return; + } + this.updateParticipation(); + } + + private updateParticipation() { + if (this.exercise().id) { + this.exerciseService.getExerciseDetails(this.exercise().id!).subscribe((exerciseResponse: HttpResponse) => { + this.participation = this.participationService.getSpecificStudentParticipation(exerciseResponse.body!.exercise.studentParticipations ?? [], false); + }); + } + } + + requestFeedback() { + if (!this.assureConditionsSatisfied()) return; + if (this.exercise().type === ExerciseType.PROGRAMMING) { + const confirmLockRepository = this.translateService.instant('artemisApp.exercise.lockRepositoryWarning'); + if (!window.confirm(confirmLockRepository)) { + return; + } + } + + this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ + next: (participation: StudentParticipation) => { + if (participation) { + this.generatingFeedback.emit(); + this.feedbackSent = true; + this.alertService.success('artemisApp.exercise.feedbackRequestSent'); + } + }, + error: (error) => { + this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); + }, + }); + } + + /** + * Checks if the conditions for requesting automatic non-graded feedback are satisfied. + * The student can request automatic non-graded feedback under the following conditions: + * 1. They have a graded submission. + * 2. The deadline for the exercise has not been exceeded. + * 3. There is no already pending feedback request. + * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. + */ + assureConditionsSatisfied(): boolean { + this.updateParticipation(); + if (this.exercise().type === ExerciseType.PROGRAMMING) { + const latestResult = this.participation?.results && this.participation.results.find(({ assessmentType }) => assessmentType === AssessmentType.AUTOMATIC); + const scoreNotNull = latestResult?.score !== undefined; + const testsNotPassedWarning = this.translateService.instant('artemisApp.exercise.submissionExists'); + if (!scoreNotNull) { + window.alert(testsNotPassedWarning); + return false; + } + } + + const afterDueDate = !this.exercise().dueDate || dayjs().isSameOrAfter(this.exercise().dueDate); + const dueDateWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAfterDueDate'); + if (afterDueDate) { + this.alertService.warning(dueDateWarning); + return false; + } + + const requestAlreadySent = (this.participation?.individualDueDate && this.participation.individualDueDate.isBefore(Date.now())) ?? false; + const requestAlreadySentWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAlreadySent'); + if (requestAlreadySent) { + this.alertService.warning(requestAlreadySentWarning); + return false; + } + + if (this.participation?.results) { + const athenaResults = this.participation.results!.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA'); + const countOfSuccessfulRequests = athenaResults.length; + + if (countOfSuccessfulRequests >= 10) { + const rateLimitExceededWarning = this.translateService.instant('artemisApp.exercise.maxAthenaResultsReached'); + this.alertService.warning(rateLimitExceededWarning); + return false; + } + } + + if (this.hasAthenaResultForLatestSubmission()) { + const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); + this.alertService.warning(submitFirstWarning); + return false; + } + return true; + } + + hasAthenaResultForLatestSubmission(): boolean { + if (this.participation?.submissions && this.participation?.results) { + // submissions.results is always undefined so this is neccessary + return ( + this.participation.submissions?.last()?.id === + this.participation.results?.filter((result) => result.assessmentType == AssessmentType.AUTOMATIC_ATHENA).first()?.submission?.id + ); + } + return false; + } +} diff --git a/src/main/webapp/i18n/de/exercise.json b/src/main/webapp/i18n/de/exercise.json index d8801ed0bb5e..dbc4098afd29 100644 --- a/src/main/webapp/i18n/de/exercise.json +++ b/src/main/webapp/i18n/de/exercise.json @@ -168,7 +168,7 @@ "resumeProgrammingExercise": "Die Aufgabe wurde wieder aufgenommen. Du kannst nun weiterarbeiten!", "feedbackRequestSent": "Deine Feedbackanfrage wurde gesendet.", "feedbackRequestAlreadySent": "Deine Feedbackanfrage wurde bereits gesendet.", - "notEnoughPoints": "Um eine Feedbackanfrage zu senden, brauchst du mindestens eine Abgabe.", + "submissionExists": "Um eine Feedbackanfrage zu senden, brauchst du mindestens eine Abgabe.", "lockRepositoryWarning": "Dein Repository wird gesperrt. Du kannst erst weiterarbeiten wenn deine Feedbackanfrage beantwortet wird.", "feedbackRequestAfterDueDate": "Du kannst nach der Abgabefrist keine weiteren Anfragen einreichen.", "maxAthenaResultsReached": "Du hast die maximale Anzahl an KI-Feedbackanfragen erreicht.", diff --git a/src/main/webapp/i18n/en/exercise.json b/src/main/webapp/i18n/en/exercise.json index 6a06631fc343..b2816f7c7eb8 100644 --- a/src/main/webapp/i18n/en/exercise.json +++ b/src/main/webapp/i18n/en/exercise.json @@ -168,7 +168,7 @@ "resumeProgrammingExercise": "The exercise has been resumed. You can now continue working on the exercise!", "feedbackRequestSent": "Your feedback request has been sent.", "feedbackRequestAlreadySent": "Your feedback request has already been sent.", - "notEnoughPoints": "You have to submit your work at least once.", + "submissionExists": "You have to submit your work at least once.", "lockRepositoryWarning": "Your repository will be locked. You can only continue working after you receive an answer.", "feedbackRequestAfterDueDate": "You cannot submit feedback requests after the due date.", "maxAthenaResultsReached": "You have reached the maximum number of AI feedback requests.", From 9711dcc5bb45777633f1b60c0ce71b35cfc3e857 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Tue, 17 Sep 2024 13:37:48 +0200 Subject: [PATCH 02/28] set the number of feedback requests to 20 --- .../request-feedback-button.component.html | 2 +- .../request-feedback-button.component.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html index b5f24d538924..b29696ab6cef 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html @@ -4,7 +4,7 @@ class="btn btn-primary" (click)="requestFeedback()" [class.btn-sm]="smallButtons()" - [disabled]="!participation?.submissions?.last()?.submitted || isGeneratingFeedback" + [disabled]="!participation?.submissions?.last()?.submitted || isGeneratingFeedback()" [id]="'request-feedback-' + exercise().id" [ngbTooltip]="'artemisApp.exerciseActions.requestAutomaticFeedbackTooltip' | artemisTranslate" > diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index 5c40f24c8189..d23b1d973db9 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -122,17 +122,17 @@ export class RequestFeedbackButtonComponent implements OnInit { } if (this.participation?.results) { - const athenaResults = this.participation.results!.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA'); + const athenaResults = this.participation.results!.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA' && result.successful); const countOfSuccessfulRequests = athenaResults.length; - if (countOfSuccessfulRequests >= 10) { + if (countOfSuccessfulRequests >= 20) { const rateLimitExceededWarning = this.translateService.instant('artemisApp.exercise.maxAthenaResultsReached'); this.alertService.warning(rateLimitExceededWarning); return false; } } - if (this.hasAthenaResultForLatestSubmission()) { + if (this.exercise().type !== ExerciseType.PROGRAMMING && this.hasAthenaResultForLatestSubmission()) { const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); this.alertService.warning(submitFirstWarning); return false; From 1f949c2aa43289e23b2051a3892f63987abb42f0 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Tue, 17 Sep 2024 14:18:53 +0200 Subject: [PATCH 03/28] change animation and fix error messages --- ...mingExerciseCodeReviewFeedbackService.java | 2 +- .../participation/participation.utils.ts | 11 ++++++++-- .../shared/result/result.component.html | 14 +++++------- .../shared/result/result.component.ts | 9 +++++++- .../exercises/shared/result/result.service.ts | 6 ++--- .../exercises/shared/result/result.utils.ts | 22 +++++++++++-------- .../result/updating-result.component.ts | 13 +++++++---- .../course-exercise-details.component.ts | 7 +++--- 8 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 8c92446d22d0..869a6e4ffe96 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -229,7 +229,7 @@ private void checkRateLimitOrThrow(ProgrammingExerciseStudentParticipation parti long countOfSuccessfulRequests = athenaResults.stream().filter(result -> result.isSuccessful() == Boolean.TRUE).count(); - if (countOfAthenaResultsInProcessOrSuccessful >= 3) { + if (countOfAthenaResultsInProcessOrSuccessful >= 20) { throw new BadRequestAlertException("Cannot send additional AI feedback requests now. Try again later!", "participation", "preconditions not met"); } if (countOfSuccessfulRequests >= 20) { diff --git a/src/main/webapp/app/exercises/shared/participation/participation.utils.ts b/src/main/webapp/app/exercises/shared/participation/participation.utils.ts index 5fc349f22b27..fac8c117f2f5 100644 --- a/src/main/webapp/app/exercises/shared/participation/participation.utils.ts +++ b/src/main/webapp/app/exercises/shared/participation/participation.utils.ts @@ -6,6 +6,7 @@ import dayjs from 'dayjs/esm'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { Result } from 'app/entities/result.model'; import { orderBy as _orderBy } from 'lodash-es'; +import { isAIResultAndIsBeingProcessed } from 'app/exercises/shared/result/result.utils'; /** * Check if the participation has changed. @@ -102,7 +103,11 @@ export const isParticipationInDueTime = (participation: Participation, exercise: * @param participation * @param showUngradedResults */ -export function getLatestResultOfStudentParticipation(participation: StudentParticipation | undefined, showUngradedResults: boolean): Result | undefined { +export function getLatestResultOfStudentParticipation( + participation: StudentParticipation | undefined, + showUngradedResults: boolean, + showAthenaPreliminaryFeedback: boolean = false, +): Result | undefined { if (!participation) { return undefined; } @@ -112,7 +117,9 @@ export function getLatestResultOfStudentParticipation(participation: StudentPart participation.results = _orderBy(participation.results, 'completionDate', 'desc'); } // The latest result is the first rated result in the sorted array (=newest) or any result if the option is active to show ungraded results. - const latestResult = participation.results?.find(({ rated }) => showUngradedResults || rated === true); + const latestResult = participation.results?.find( + (result) => showUngradedResults || result.rated === true || (showAthenaPreliminaryFeedback && !!isAIResultAndIsBeingProcessed(result)), + ); // Make sure that the participation result is connected to the newest result. return latestResult ? { ...latestResult, participation: participation } : undefined; } diff --git a/src/main/webapp/app/exercises/shared/result/result.component.html b/src/main/webapp/app/exercises/shared/result/result.component.html index 2dfd17685054..f31d9c987090 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.html +++ b/src/main/webapp/app/exercises/shared/result/result.component.html @@ -20,14 +20,10 @@ } } @case (ResultTemplateStatus.IS_GENERATING_FEEDBACK) { - @if (result) { - - - - {{ resultString }} - - - } + + + + } @case (ResultTemplateStatus.FEEDBACK_GENERATION_TIMED_OUT) { @if (result) { @@ -59,7 +55,7 @@ } @if (!isInSidebarCard) { - ({{ result!.completionDate | artemisTimeAgo }}) + ({{ result!.completionDate | artemisTimeAgo }} ) } @if (hasBuildArtifact() && participation.type === ParticipationType.PROGRAMMING) { diff --git a/src/main/webapp/app/exercises/shared/result/result.component.ts b/src/main/webapp/app/exercises/shared/result/result.component.ts index 3415021e11c7..c26bb427fa68 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/result.component.ts @@ -1,6 +1,13 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, Optional, SimpleChanges } from '@angular/core'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; -import { MissingResultInformation, ResultTemplateStatus, evaluateTemplateStatus, getResultIconClass, getTextColorClass } from 'app/exercises/shared/result/result.utils'; +import { + MissingResultInformation, + ResultTemplateStatus, + evaluateTemplateStatus, + getResultIconClass, + getTextColorClass, + isAIResultAndFailed, +} from 'app/exercises/shared/result/result.utils'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index 2fd62aa574b7..2e3c35837532 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -149,9 +149,7 @@ export class ResultService implements IResultService { */ private getResultStringProgrammingExercise(result: Result, exercise: ProgrammingExercise, relativeScore: number, points: number, short: boolean | undefined): string { let buildAndTestMessage: string; - if (result.submission && (result.submission as ProgrammingSubmission).buildFailed) { - buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.buildFailed'); - } else if (isAIResultAndFailed(result)) { + if (isAIResultAndFailed(result)) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackFailed'); } else if (isAIResultAndIsBeingProcessed(result)) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackInProgress'); @@ -159,6 +157,8 @@ export class ResultService implements IResultService { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackTimedOut'); } else if (isAIResultAndProcessed(result)) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackSuccessful'); + } else if (result.submission && (result.submission as ProgrammingSubmission).buildFailed) { + buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.buildFailed'); } else if (!result.testCaseCount) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.buildSuccessfulNoTests'); } else { diff --git a/src/main/webapp/app/exercises/shared/result/result.utils.ts b/src/main/webapp/app/exercises/shared/result/result.utils.ts index e8ebbf03e898..1babd285d353 100644 --- a/src/main/webapp/app/exercises/shared/result/result.utils.ts +++ b/src/main/webapp/app/exercises/shared/result/result.utils.ts @@ -248,9 +248,12 @@ export const getTextColorClass = (result: Result | undefined, templateStatus: Re } if (result.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { - if (result.successful == undefined) { + if (isAIResultAndIsBeingProcessed(result)) { return 'text-primary'; } + if (isAIResultAndFailed(result)) { + return 'text-danger'; + } return 'text-secondary'; } @@ -258,11 +261,11 @@ export const getTextColorClass = (result: Result | undefined, templateStatus: Re return 'result-late'; } - if (isBuildFailedAndResultIsAutomatic(result) || isAIResultAndFailed(result)) { + if (isBuildFailedAndResultIsAutomatic(result)) { return 'text-danger'; } - if (resultIsPreliminary(result) || isAIResultAndIsBeingProcessed(result) || isAIResultAndTimedOut(result)) { + if (resultIsPreliminary(result)) { return 'text-secondary'; } @@ -294,18 +297,19 @@ export const getResultIconClass = (result: Result | undefined, templateStatus: R return faQuestionCircle; } - if (result.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { - if (result.successful === undefined) { - return faCircleNotch; - } - return faQuestionCircle; + if (isAIResultAndProcessed(result)) { + return faCheckCircle; } if (isBuildFailedAndResultIsAutomatic(result) || isAIResultAndFailed(result)) { return faTimesCircle; } - if (resultIsPreliminary(result) || isAIResultAndTimedOut(result) || isAIResultAndIsBeingProcessed(result)) { + if (isAIResultAndIsBeingProcessed(result)) { + return faCircleNotch; + } + + if (resultIsPreliminary(result) || isAIResultAndTimedOut(result)) { return faQuestionCircle; } diff --git a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts index 55cde780b0ec..f64acf1cced0 100644 --- a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts @@ -13,7 +13,7 @@ import { StudentParticipation } from 'app/entities/participation/student-partici import { Result } from 'app/entities/result.model'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { getLatestResultOfStudentParticipation, hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; -import { MissingResultInformation } from 'app/exercises/shared/result/result.utils'; +import { isAIResultAndIsBeingProcessed, MissingResultInformation } from 'app/exercises/shared/result/result.utils'; import { convertDateFromServer } from 'app/utils/date.utils'; /** @@ -59,7 +59,7 @@ export class UpdatingResultComponent implements OnChanges, OnDestroy { */ ngOnChanges(changes: SimpleChanges) { if (hasParticipationChanged(changes)) { - this.result = getLatestResultOfStudentParticipation(this.participation, this.showUngradedResults); + this.result = getLatestResultOfStudentParticipation(this.participation, this.showUngradedResults, true); this.missingResultInfo = MissingResultInformation.NONE; this.subscribeForNewResults(); @@ -101,10 +101,15 @@ export class UpdatingResultComponent implements OnChanges, OnDestroy { // Ignore initial null result of subscription filter((result) => !!result), // Ignore ungraded results if ungraded results are supposed to be ignored. - filter((result: Result) => this.showUngradedResults || result.rated === true), + // If the result is a preliminary feedback(being generated), show it + filter((result: Result) => this.showUngradedResults || result.rated === true || Result.isAthenaAIResult(result)), map((result) => ({ ...result, completionDate: convertDateFromServer(result.completionDate), participation: this.participation })), tap((result) => { - this.result = result; + if ((Result.isAthenaAIResult(result) && isAIResultAndIsBeingProcessed(result)) || result.rated) { + this.result = result; + } else { + this.result = getLatestResultOfStudentParticipation(this.participation, this.showUngradedResults, false); + } this.onParticipationChange.emit(); if (result) { this.showResult.emit(); diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index 62a1b6fa7429..8a3038bad905 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -1,7 +1,7 @@ import { Component, ContentChild, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; -import { Subscription, combineLatest } from 'rxjs'; +import { combineLatest, Subscription } from 'rxjs'; import { filter, skip } from 'rxjs/operators'; import { Result } from 'app/entities/result.model'; import dayjs from 'dayjs/esm'; @@ -198,7 +198,6 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.exerciseCategories = this.exercise.categories ?? []; this.allowComplaintsForAutomaticAssessments = false; this.plagiarismCaseInfo = newExerciseDetails.plagiarismCaseInfo; - if (this.exercise.type === ExerciseType.PROGRAMMING) { const programmingExercise = this.exercise as ProgrammingExercise; const isAfterDateForComplaint = @@ -243,7 +242,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp private filterUnfinishedResults(participations?: StudentParticipation[]) { participations?.forEach((participation: Participation) => { if (participation.results) { - participation.results = participation.results.filter((result: Result) => result.completionDate && result.successful !== undefined); + participation.results = participation.results.filter((result: Result) => result.completionDate); } }); } @@ -254,7 +253,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.sortedHistoryResults = this.studentParticipations .flatMap((participation) => participation.results ?? []) .sort(this.resultSortFunction) - .filter((result) => !(result.assessmentType === AssessmentType.AUTOMATIC_ATHENA && result.successful == undefined)); + .filter((result) => !(result.assessmentType === AssessmentType.AUTOMATIC_ATHENA && dayjs().isBefore(result.completionDate))); } } From 02987d0173fd6bdfb174abfbffd0c85c6a318189 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Tue, 17 Sep 2024 15:42:29 +0200 Subject: [PATCH 04/28] remove individual due date --- .../exercise/web/ParticipationResource.java | 4 +- ...mingExerciseCodeReviewFeedbackService.java | 9 +- .../utils/programming-exercise.utils.ts | 5 +- .../shared/feedback/feedback.component.html | 14 ++- .../request-feedback-button.component.ts | 103 +++++++++--------- src/main/webapp/i18n/de/result.json | 1 + src/main/webapp/i18n/en/result.json | 1 + 7 files changed, 73 insertions(+), 64 deletions(-) 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..21422b1a6c85 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 @@ -406,8 +406,8 @@ else if (exercise instanceof ProgrammingExercise) { // Check if feedback has already been requested var currentDate = now(); - var participationIndividualDueDate = participation.getIndividualDueDate(); - if (participationIndividualDueDate != null && currentDate.isAfter(participationIndividualDueDate)) { + var latestResult = participation.findLatestResult(); + if (latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA && latestResult.getCompletionDate().isAfter(now())) { throw new BadRequestAlertException("Request has already been sent", "participation", "already sent"); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 869a6e4ffe96..740cc68f231b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -4,6 +4,7 @@ import static java.time.ZonedDateTime.now; import java.time.ZonedDateTime; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -127,7 +128,6 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici try { - setIndividualDueDateAndLockRepository(participation, programmingExercise, false); this.programmingMessagingService.notifyUserAboutNewResult(automaticResult, participation); // now the client should be able to see new result @@ -158,9 +158,9 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici feedback.setDetailText(individualFeedbackItem.description()); feedback.setHasLongFeedbackText(false); feedback.setType(FeedbackType.AUTOMATIC); - feedback.setCredits(0.0); + feedback.setCredits(individualFeedbackItem.credits()); return feedback; - }).toList(); + }).sorted(Comparator.comparing(Feedback::getCredits)).toList(); automaticResult.setSuccessful(true); automaticResult.setCompletionDate(ZonedDateTime.now()); @@ -176,9 +176,6 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici this.resultRepository.save(automaticResult); this.programmingMessagingService.notifyUserAboutNewResult(automaticResult, participation); } - finally { - unlockRepository(participation, programmingExercise); - } } /** diff --git a/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts b/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts index 5e95bb66dbac..4e99d717f019 100644 --- a/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts +++ b/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts @@ -7,6 +7,7 @@ import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { isPracticeMode } from 'app/entities/participation/student-participation.model'; +import { isAIResultAndProcessed } from 'app/exercises/shared/result/result.utils'; export const createBuildPlanUrl = (template: string, projectKey: string, buildPlanId: string): string | undefined => { if (template && projectKey && buildPlanId) { @@ -59,8 +60,8 @@ export const isResultPreliminary = (latestResult: Result, programmingExercise?: if (!programmingExercise) { return false; } - if (latestResult.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { - return false; + if (isAIResultAndProcessed(latestResult)) { + return true; } if (latestResult.participation?.type === ParticipationType.PROGRAMMING && isPracticeMode(latestResult.participation)) { return false; 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..f126b6d7679f 100644 --- a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html +++ b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html @@ -119,11 +119,15 @@

{{ 'artemisApp.result.preliminary' | artemisTranslate | uppercase }}
- @if (exercise?.assessmentType !== AssessmentType.AUTOMATIC) { -

- } - @if (exercise?.assessmentType === AssessmentType.AUTOMATIC) { -

+ @if (result?.assessmentType !== AssessmentType.AUTOMATIC_ATHENA) { + @if (exercise?.assessmentType !== AssessmentType.AUTOMATIC) { +

+ } + @if (exercise?.assessmentType === AssessmentType.AUTOMATIC) { +

+ } + } @else { +

} } diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index d23b1d973db9..195177e41b6f 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -1,4 +1,4 @@ -import { Component, input, inject, OnInit, output } from '@angular/core'; +import { Component, inject, input, OnInit, output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; @@ -17,6 +17,8 @@ import { isExamExercise } from 'app/shared/util/utils'; import { ExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { HttpResponse } from '@angular/common/http'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; @Component({ selector: 'jhi-request-feedback-button', @@ -56,22 +58,20 @@ export class RequestFeedbackButtonComponent implements OnInit { this.updateParticipation(); } - private updateParticipation() { + private updateParticipation(): Observable { if (this.exercise().id) { - this.exerciseService.getExerciseDetails(this.exercise().id!).subscribe((exerciseResponse: HttpResponse) => { - this.participation = this.participationService.getSpecificStudentParticipation(exerciseResponse.body!.exercise.studentParticipations ?? [], false); - }); + return this.exerciseService.getExerciseDetails(this.exercise().id!).pipe( + map((exerciseResponse: HttpResponse) => { + this.participation = this.participationService.getSpecificStudentParticipation(exerciseResponse.body!.exercise.studentParticipations ?? [], false); + return this.participation; + }), + ); } + return of(undefined); } requestFeedback() { if (!this.assureConditionsSatisfied()) return; - if (this.exercise().type === ExerciseType.PROGRAMMING) { - const confirmLockRepository = this.translateService.instant('artemisApp.exercise.lockRepositoryWarning'); - if (!window.confirm(confirmLockRepository)) { - return; - } - } this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ next: (participation: StudentParticipation) => { @@ -95,49 +95,54 @@ export class RequestFeedbackButtonComponent implements OnInit { * 3. There is no already pending feedback request. * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. */ - assureConditionsSatisfied(): boolean { - this.updateParticipation(); - if (this.exercise().type === ExerciseType.PROGRAMMING) { - const latestResult = this.participation?.results && this.participation.results.find(({ assessmentType }) => assessmentType === AssessmentType.AUTOMATIC); - const scoreNotNull = latestResult?.score !== undefined; - const testsNotPassedWarning = this.translateService.instant('artemisApp.exercise.submissionExists'); - if (!scoreNotNull) { - window.alert(testsNotPassedWarning); - return false; - } - } + assureConditionsSatisfied(): Observable { + return this.updateParticipation().pipe( + map(() => { + const latestResult = this.participation?.results?.find(({ assessmentType }) => assessmentType === AssessmentType.AUTOMATIC); - const afterDueDate = !this.exercise().dueDate || dayjs().isSameOrAfter(this.exercise().dueDate); - const dueDateWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAfterDueDate'); - if (afterDueDate) { - this.alertService.warning(dueDateWarning); - return false; - } + if (this.exercise().type === ExerciseType.PROGRAMMING) { + const scoreNotNull = latestResult?.score !== undefined; + const testsNotPassedWarning = this.translateService.instant('artemisApp.exercise.submissionExists'); + if (!scoreNotNull) { + window.alert(testsNotPassedWarning); + return false; + } + } - const requestAlreadySent = (this.participation?.individualDueDate && this.participation.individualDueDate.isBefore(Date.now())) ?? false; - const requestAlreadySentWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAlreadySent'); - if (requestAlreadySent) { - this.alertService.warning(requestAlreadySentWarning); - return false; - } + const afterDueDate = !this.exercise().dueDate || dayjs().isSameOrAfter(this.exercise().dueDate); + const dueDateWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAfterDueDate'); + if (afterDueDate) { + this.alertService.warning(dueDateWarning); + return false; + } - if (this.participation?.results) { - const athenaResults = this.participation.results!.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA' && result.successful); - const countOfSuccessfulRequests = athenaResults.length; + const requestAlreadySent = (latestResult?.assessmentType === AssessmentType.AUTOMATIC_ATHENA && dayjs().isBefore(latestResult?.completionDate)) ?? false; + const requestAlreadySentWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAlreadySent'); + if (requestAlreadySent) { + this.alertService.warning(requestAlreadySentWarning); + return false; + } - if (countOfSuccessfulRequests >= 20) { - const rateLimitExceededWarning = this.translateService.instant('artemisApp.exercise.maxAthenaResultsReached'); - this.alertService.warning(rateLimitExceededWarning); - return false; - } - } + if (this.participation?.results) { + const athenaResults = this.participation.results!.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA' && result.successful); + const countOfSuccessfulRequests = athenaResults.length; - if (this.exercise().type !== ExerciseType.PROGRAMMING && this.hasAthenaResultForLatestSubmission()) { - const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); - this.alertService.warning(submitFirstWarning); - return false; - } - return true; + if (countOfSuccessfulRequests >= 20) { + const rateLimitExceededWarning = this.translateService.instant('artemisApp.exercise.maxAthenaResultsReached'); + this.alertService.warning(rateLimitExceededWarning); + return false; + } + } + + if (this.exercise().type !== ExerciseType.PROGRAMMING && this.hasAthenaResultForLatestSubmission()) { + const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); + this.alertService.warning(submitFirstWarning); + return false; + } + + return true; + }), + ); } hasAthenaResultForLatestSubmission(): boolean { diff --git a/src/main/webapp/i18n/de/result.json b/src/main/webapp/i18n/de/result.json index e414943fe65e..99a36a0d4057 100644 --- a/src/main/webapp/i18n/de/result.json +++ b/src/main/webapp/i18n/de/result.json @@ -92,6 +92,7 @@ "preliminary": "vorläufig", "preliminaryTooltip": "Dein Ergebnis ist noch nicht endgültig, weil weitere Tests nach der Einreichungsfrist ausgeführt werden.", "preliminaryTooltipSemiAutomatic": "Dein Ergebnis ist noch nicht endgültig, weil weitere Tests nach der Einreichungsfrist ausgeführt werden oder eine manuelle Bewertung aussteht.", + "preliminaryTooltipAthena": "Dies ist eine Bewertung durch die KI, das tatsächliche Ergebnis kann abweichen.", "codeIssuesTooltip": "Die automatische Codeanalyse hat Codeissues gefunden.", "noResultDetails": "Keine weiteren Informationen verfügbar für dieses Ergebnis.", "onlyCompilationTested": "Dein Code kompiliert erfolgreich. Derzeit sind keine Testfälle sichtbar.", diff --git a/src/main/webapp/i18n/en/result.json b/src/main/webapp/i18n/en/result.json index 67bdc0711adb..6f5047025659 100644 --- a/src/main/webapp/i18n/en/result.json +++ b/src/main/webapp/i18n/en/result.json @@ -92,6 +92,7 @@ "preliminary": "preliminary", "preliminaryTooltip": "Your result is not final yet, because more tests will be executed after the due date", "preliminaryTooltipSemiAutomatic": "Your result is not final yet, because more tests will be executed after the due date or a manual assessment will be done.", + "preliminaryTooltipAthena": "This is a grading by the AI, the actual result may differ", "codeIssuesTooltip": "The automatic code analysis generated some warnings for your code.", "noResultDetails": "No result details available.", "onlyCompilationTested": "Your code compiled successfully. There are currently no tests visible.", From d0f9539d1787fa3d9bad6ac7ad999a788ab2c8b8 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Tue, 17 Sep 2024 15:59:21 +0200 Subject: [PATCH 05/28] use async call chain to request feedback --- .../request-feedback-button.component.ts | 128 ++++++++++-------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index d23b1d973db9..2a94779f6dda 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -17,6 +17,8 @@ import { isExamExercise } from 'app/shared/util/utils'; import { ExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { HttpResponse } from '@angular/common/http'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; @Component({ selector: 'jhi-request-feedback-button', @@ -56,34 +58,43 @@ export class RequestFeedbackButtonComponent implements OnInit { this.updateParticipation(); } - private updateParticipation() { + private updateParticipation(): Observable { if (this.exercise().id) { - this.exerciseService.getExerciseDetails(this.exercise().id!).subscribe((exerciseResponse: HttpResponse) => { - this.participation = this.participationService.getSpecificStudentParticipation(exerciseResponse.body!.exercise.studentParticipations ?? [], false); - }); + return this.exerciseService.getExerciseDetails(this.exercise().id!).pipe( + map((exerciseResponse: HttpResponse) => { + this.participation = this.participationService.getSpecificStudentParticipation(exerciseResponse.body!.exercise.studentParticipations ?? [], false); + return this.participation; + }), + ); } + return of(undefined); } requestFeedback() { - if (!this.assureConditionsSatisfied()) return; - if (this.exercise().type === ExerciseType.PROGRAMMING) { - const confirmLockRepository = this.translateService.instant('artemisApp.exercise.lockRepositoryWarning'); - if (!window.confirm(confirmLockRepository)) { + this.assureConditionsSatisfied().subscribe((conditionsSatisfied: boolean) => { + if (!conditionsSatisfied) { return; } - } - this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ - next: (participation: StudentParticipation) => { - if (participation) { - this.generatingFeedback.emit(); - this.feedbackSent = true; - this.alertService.success('artemisApp.exercise.feedbackRequestSent'); + if (this.exercise().type === ExerciseType.PROGRAMMING) { + const confirmLockRepository = this.translateService.instant('artemisApp.exercise.lockRepositoryWarning'); + if (!window.confirm(confirmLockRepository)) { + return; } - }, - error: (error) => { - this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); - }, + } + + this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ + next: (participation: StudentParticipation) => { + if (participation) { + this.generatingFeedback.emit(); + this.feedbackSent = true; + this.alertService.success('artemisApp.exercise.feedbackRequestSent'); + } + }, + error: (error) => { + this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); + }, + }); }); } @@ -95,49 +106,52 @@ export class RequestFeedbackButtonComponent implements OnInit { * 3. There is no already pending feedback request. * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. */ - assureConditionsSatisfied(): boolean { - this.updateParticipation(); - if (this.exercise().type === ExerciseType.PROGRAMMING) { - const latestResult = this.participation?.results && this.participation.results.find(({ assessmentType }) => assessmentType === AssessmentType.AUTOMATIC); - const scoreNotNull = latestResult?.score !== undefined; - const testsNotPassedWarning = this.translateService.instant('artemisApp.exercise.submissionExists'); - if (!scoreNotNull) { - window.alert(testsNotPassedWarning); - return false; - } - } + assureConditionsSatisfied(): Observable { + return this.updateParticipation().pipe( + map(() => { + if (this.exercise().type === ExerciseType.PROGRAMMING) { + const latestResult = this.participation?.results && this.participation.results.find(({ assessmentType }) => assessmentType === AssessmentType.AUTOMATIC); + const scoreNotNull = latestResult?.score !== undefined; + const testsNotPassedWarning = this.translateService.instant('artemisApp.exercise.submissionExists'); + if (!scoreNotNull) { + window.alert(testsNotPassedWarning); + return false; + } + } - const afterDueDate = !this.exercise().dueDate || dayjs().isSameOrAfter(this.exercise().dueDate); - const dueDateWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAfterDueDate'); - if (afterDueDate) { - this.alertService.warning(dueDateWarning); - return false; - } + const afterDueDate = !this.exercise().dueDate || dayjs().isSameOrAfter(this.exercise().dueDate); + const dueDateWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAfterDueDate'); + if (afterDueDate) { + this.alertService.warning(dueDateWarning); + return false; + } - const requestAlreadySent = (this.participation?.individualDueDate && this.participation.individualDueDate.isBefore(Date.now())) ?? false; - const requestAlreadySentWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAlreadySent'); - if (requestAlreadySent) { - this.alertService.warning(requestAlreadySentWarning); - return false; - } + const requestAlreadySent = (this.participation?.individualDueDate && this.participation.individualDueDate.isBefore(Date.now())) ?? false; + const requestAlreadySentWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAlreadySent'); + if (requestAlreadySent) { + this.alertService.warning(requestAlreadySentWarning); + return false; + } - if (this.participation?.results) { - const athenaResults = this.participation.results!.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA' && result.successful); - const countOfSuccessfulRequests = athenaResults.length; + if (this.participation?.results) { + const athenaResults = this.participation.results!.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA' && result.successful); + const countOfSuccessfulRequests = athenaResults.length; - if (countOfSuccessfulRequests >= 20) { - const rateLimitExceededWarning = this.translateService.instant('artemisApp.exercise.maxAthenaResultsReached'); - this.alertService.warning(rateLimitExceededWarning); - return false; - } - } + if (countOfSuccessfulRequests >= 20) { + const rateLimitExceededWarning = this.translateService.instant('artemisApp.exercise.maxAthenaResultsReached'); + this.alertService.warning(rateLimitExceededWarning); + return false; + } + } - if (this.exercise().type !== ExerciseType.PROGRAMMING && this.hasAthenaResultForLatestSubmission()) { - const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); - this.alertService.warning(submitFirstWarning); - return false; - } - return true; + if (this.exercise().type !== ExerciseType.PROGRAMMING && this.hasAthenaResultForLatestSubmission()) { + const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); + this.alertService.warning(submitFirstWarning); + return false; + } + return true; + }), + ); } hasAthenaResultForLatestSubmission(): boolean { From 3fa94a617c295e8fc10b1da2d6972b384dbf0a3c Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Tue, 17 Sep 2024 16:02:55 +0200 Subject: [PATCH 06/28] use async call chain to request feedback --- .../request-feedback-button.component.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index 195177e41b6f..77b542fcc419 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -71,19 +71,21 @@ export class RequestFeedbackButtonComponent implements OnInit { } requestFeedback() { - if (!this.assureConditionsSatisfied()) return; - - this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ - next: (participation: StudentParticipation) => { - if (participation) { - this.generatingFeedback.emit(); - this.feedbackSent = true; - this.alertService.success('artemisApp.exercise.feedbackRequestSent'); - } - }, - error: (error) => { - this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); - }, + this.assureConditionsSatisfied().subscribe((conditionsSatisfied: boolean) => { + if (!this.assureConditionsSatisfied()) return; + + this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ + next: (participation: StudentParticipation) => { + if (participation) { + this.generatingFeedback.emit(); + this.feedbackSent = true; + this.alertService.success('artemisApp.exercise.feedbackRequestSent'); + } + }, + error: (error) => { + this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); + }, + }); }); } From fe7d0eca9d777b10526a2d5cd90ea45b734cf880 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Tue, 17 Sep 2024 17:04:03 +0200 Subject: [PATCH 07/28] generate rated results --- .../service/ProgrammingExerciseCodeReviewFeedbackService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 740cc68f231b..392d491068e6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -119,7 +119,7 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici // save result and transmit it over websockets to notify the client about the status var automaticResult = this.submissionService.saveNewEmptyResult(submission); automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); - automaticResult.setRated(false); + automaticResult.setRated(true); automaticResult.setScore(100.0); automaticResult.setSuccessful(null); automaticResult.setCompletionDate(ZonedDateTime.now().plusMinutes(5)); // we do not want to show dates without a completion date, but we want the students to know their From 6493b0e5f343f5b96eaf334afba462e5b069a7fb Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Mon, 30 Sep 2024 13:35:58 +0200 Subject: [PATCH 08/28] rework notifications --- .../exercise/web/ParticipationResource.java | 9 +- ...mingExerciseCodeReviewFeedbackService.java | 9 +- ...ise-details-student-actions.component.html | 10 +- .../request-feedback-button.component.html | 54 +++++----- .../request-feedback-button.component.ts | 98 ++++++------------- src/main/webapp/i18n/de/exercise.json | 2 +- src/main/webapp/i18n/en/exercise.json | 2 +- .../ParticipationIntegrationTest.java | 8 +- 8 files changed, 77 insertions(+), 115 deletions(-) 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 6559c28b9d93..738ef8a3f2dd 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 @@ -20,7 +20,6 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; -import org.apache.velocity.exception.ResourceNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -382,7 +381,7 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc throw new BadRequestAlertException("Not intended for the use in exams", "participation", "preconditions not met"); } if (exercise.getDueDate() != null && now().isAfter(exercise.getDueDate())) { - throw new BadRequestAlertException("The due date is over", "participation", "preconditions not met"); + throw new BadRequestAlertException("The due date is over", "participation", "feedbackRequestAfterDueDate", true); } if (exercise instanceof ProgrammingExercise) { ((ProgrammingExercise) exercise).validateSettingsForFeedbackRequest(); @@ -393,7 +392,7 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc StudentParticipation participation = (exercise instanceof ProgrammingExercise) ? programmingExerciseParticipationService.findStudentParticipationByExerciseAndStudentId(exercise, principal.getName()) : studentParticipationRepository.findByExerciseIdAndStudentLogin(exercise.getId(), principal.getName()) - .orElseThrow(() -> new ResourceNotFoundException("Participation not found")); + .orElseThrow(() -> new BadRequestAlertException("Participation not found", "participation", "noSubmissionExists", true)); checkAccessPermissionOwner(participation, user); participation = studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId()); @@ -406,7 +405,7 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc } else if (exercise instanceof ProgrammingExercise) { if (participation.findLatestLegalResult() == null) { - throw new BadRequestAlertException("User has not reached the conditions to submit a feedback request", "participation", "preconditions not met"); + throw new BadRequestAlertException("You need to submit at least once and have the build results", "participation", "noSubmissionExists", true); } } @@ -414,7 +413,7 @@ else if (exercise instanceof ProgrammingExercise) { var currentDate = now(); var participationIndividualDueDate = participation.getIndividualDueDate(); if (participationIndividualDueDate != null && currentDate.isAfter(participationIndividualDueDate)) { - throw new BadRequestAlertException("Request has already been sent", "participation", "already sent"); + throw new BadRequestAlertException("Request has already been sent", "participation", "feedbackRequestAlreadySent", true); } // Process feedback request diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 8c92446d22d0..311b4d4913e4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -111,7 +111,7 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici var submissionOptional = programmingExerciseParticipationService.findProgrammingExerciseParticipationWithLatestSubmissionAndResult(participation.getId()) .findLatestSubmission(); if (submissionOptional.isEmpty()) { - throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionÊxists"); } var submission = submissionOptional.get(); @@ -225,15 +225,10 @@ private void checkRateLimitOrThrow(ProgrammingExerciseStudentParticipation parti List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); - long countOfAthenaResultsInProcessOrSuccessful = athenaResults.stream().filter(result -> result.isSuccessful() == null || result.isSuccessful() == Boolean.TRUE).count(); - long countOfSuccessfulRequests = athenaResults.stream().filter(result -> result.isSuccessful() == Boolean.TRUE).count(); - if (countOfAthenaResultsInProcessOrSuccessful >= 3) { - throw new BadRequestAlertException("Cannot send additional AI feedback requests now. Try again later!", "participation", "preconditions not met"); - } if (countOfSuccessfulRequests >= 20) { - throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met"); + throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); } } } diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html index e032b1ca293c..6e2df76cbef9 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html @@ -66,7 +66,7 @@ } - + @case (ExerciseType.PROGRAMMING) {
@if (isTeamAvailable) { @@ -144,7 +144,7 @@ @if ( (gradedParticipation?.initializationState === InitializationState.INACTIVE || gradedParticipation?.initializationState === InitializationState.FINISHED) && isResumeExerciseAvailable(gradedParticipation) - ) { + ) { +@if (!isExamExercise) { + @if (athenaEnabled) { + @if (exercise().type === ExerciseType.TEXT) { + + } @else { + + + + + } } @else { - + } -} @else { - - - - } diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index 2a94779f6dda..4000f7c54919 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -15,7 +15,7 @@ import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; import dayjs from 'dayjs/esm'; import { isExamExercise } from 'app/shared/util/utils'; import { ExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { HttpResponse } from '@angular/common/http'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -71,30 +71,28 @@ export class RequestFeedbackButtonComponent implements OnInit { } requestFeedback() { - this.assureConditionsSatisfied().subscribe((conditionsSatisfied: boolean) => { - if (!conditionsSatisfied) { + if (!this.assureConditionsSatisfied()) { + return; + } + + if (this.exercise().type === ExerciseType.PROGRAMMING) { + const confirmLockRepository = this.translateService.instant('artemisApp.exercise.lockRepositoryWarning'); + if (!window.confirm(confirmLockRepository)) { return; } + } - if (this.exercise().type === ExerciseType.PROGRAMMING) { - const confirmLockRepository = this.translateService.instant('artemisApp.exercise.lockRepositoryWarning'); - if (!window.confirm(confirmLockRepository)) { - return; + this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ + next: (participation: StudentParticipation) => { + if (participation) { + this.generatingFeedback.emit(); + this.feedbackSent = true; + this.alertService.success('artemisApp.exercise.feedbackRequestSent'); } - } - - this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ - next: (participation: StudentParticipation) => { - if (participation) { - this.generatingFeedback.emit(); - this.feedbackSent = true; - this.alertService.success('artemisApp.exercise.feedbackRequestSent'); - } - }, - error: (error) => { - this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); - }, - }); + }, + error: (error: HttpErrorResponse) => { + this.alertService.error(`artemisApp.exercise.${error.error.errorKey}`); + }, }); } @@ -106,52 +104,13 @@ export class RequestFeedbackButtonComponent implements OnInit { * 3. There is no already pending feedback request. * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. */ - assureConditionsSatisfied(): Observable { - return this.updateParticipation().pipe( - map(() => { - if (this.exercise().type === ExerciseType.PROGRAMMING) { - const latestResult = this.participation?.results && this.participation.results.find(({ assessmentType }) => assessmentType === AssessmentType.AUTOMATIC); - const scoreNotNull = latestResult?.score !== undefined; - const testsNotPassedWarning = this.translateService.instant('artemisApp.exercise.submissionExists'); - if (!scoreNotNull) { - window.alert(testsNotPassedWarning); - return false; - } - } - - const afterDueDate = !this.exercise().dueDate || dayjs().isSameOrAfter(this.exercise().dueDate); - const dueDateWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAfterDueDate'); - if (afterDueDate) { - this.alertService.warning(dueDateWarning); - return false; - } - - const requestAlreadySent = (this.participation?.individualDueDate && this.participation.individualDueDate.isBefore(Date.now())) ?? false; - const requestAlreadySentWarning = this.translateService.instant('artemisApp.exercise.feedbackRequestAlreadySent'); - if (requestAlreadySent) { - this.alertService.warning(requestAlreadySentWarning); - return false; - } - - if (this.participation?.results) { - const athenaResults = this.participation.results!.filter((result) => result.assessmentType === 'AUTOMATIC_ATHENA' && result.successful); - const countOfSuccessfulRequests = athenaResults.length; - - if (countOfSuccessfulRequests >= 20) { - const rateLimitExceededWarning = this.translateService.instant('artemisApp.exercise.maxAthenaResultsReached'); - this.alertService.warning(rateLimitExceededWarning); - return false; - } - } - - if (this.exercise().type !== ExerciseType.PROGRAMMING && this.hasAthenaResultForLatestSubmission()) { - const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); - this.alertService.warning(submitFirstWarning); - return false; - } - return true; - }), - ); + assureConditionsSatisfied(): boolean { + if (this.exercise().type !== ExerciseType.PROGRAMMING && this.hasAthenaResultForLatestSubmission()) { + const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); + this.alertService.warning(submitFirstWarning); + return false; + } + return true; } hasAthenaResultForLatestSubmission(): boolean { @@ -165,3 +124,8 @@ export class RequestFeedbackButtonComponent implements OnInit { return false; } } + +// tests: athena enabled, athena disabled if I can see the buttons +// check disabled status +// check names, symbols, icons +// click effect diff --git a/src/main/webapp/i18n/de/exercise.json b/src/main/webapp/i18n/de/exercise.json index dbc4098afd29..363b5198a69c 100644 --- a/src/main/webapp/i18n/de/exercise.json +++ b/src/main/webapp/i18n/de/exercise.json @@ -168,7 +168,7 @@ "resumeProgrammingExercise": "Die Aufgabe wurde wieder aufgenommen. Du kannst nun weiterarbeiten!", "feedbackRequestSent": "Deine Feedbackanfrage wurde gesendet.", "feedbackRequestAlreadySent": "Deine Feedbackanfrage wurde bereits gesendet.", - "submissionExists": "Um eine Feedbackanfrage zu senden, brauchst du mindestens eine Abgabe.", + "noSubmissionExists": "Um eine Feedbackanfrage zu senden, brauchst du mindestens eine Abgabe.", "lockRepositoryWarning": "Dein Repository wird gesperrt. Du kannst erst weiterarbeiten wenn deine Feedbackanfrage beantwortet wird.", "feedbackRequestAfterDueDate": "Du kannst nach der Abgabefrist keine weiteren Anfragen einreichen.", "maxAthenaResultsReached": "Du hast die maximale Anzahl an KI-Feedbackanfragen erreicht.", diff --git a/src/main/webapp/i18n/en/exercise.json b/src/main/webapp/i18n/en/exercise.json index b2816f7c7eb8..f50e61efcbb8 100644 --- a/src/main/webapp/i18n/en/exercise.json +++ b/src/main/webapp/i18n/en/exercise.json @@ -168,7 +168,7 @@ "resumeProgrammingExercise": "The exercise has been resumed. You can now continue working on the exercise!", "feedbackRequestSent": "Your feedback request has been sent.", "feedbackRequestAlreadySent": "Your feedback request has already been sent.", - "submissionExists": "You have to submit your work at least once.", + "noSubmissionExists": "You have to submit your work at least once.", "lockRepositoryWarning": "Your repository will be locked. You can only continue working after you receive an answer.", "feedbackRequestAfterDueDate": "You cannot submit feedback requests after the due date.", "maxAthenaResultsReached": "You have reached the maximum number of AI feedback requests.", diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java index 03beb447adad..6fa5a1146440 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; @@ -1615,7 +1616,7 @@ void whenFeedbackRequestedAndDeadlinePassed_thenFail() throws Exception { result.setCompletionDate(ZonedDateTime.now()); resultRepository.save(result); - request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); + request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "feedbackRequestAfterDueDate"); localRepo.resetLocalRepo(); } @@ -1643,18 +1644,19 @@ void whenFeedbackRequestedAndRateLimitExceeded_thenFail() throws Exception { resultRepository.save(result); // generate 5 athena results - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 20; i++) { var athenaResult = ParticipationFactory.generateResult(false, 100).participation(participation); athenaResult.setCompletionDate(ZonedDateTime.now()); athenaResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); resultRepository.save(athenaResult); } - request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); + request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "maxAthenaResultsReached"); localRepo.resetLocalRepo(); } + @Disabled // will be re-enabled in https://github.com/ls1intum/Artemis/pull/9324/ @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void whenFeedbackRequestedAndRateLimitStillUnknownDueRequestsInProgress_thenFail() throws Exception { From 42f6b39ae46ff15cb3fd37bbdba7ec4f1b478a0f Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Mon, 30 Sep 2024 15:59:33 +0200 Subject: [PATCH 09/28] implement tests, change subscription type --- .../request-feedback-button.component.ts | 20 +- .../request-feedback-button.component.spec.ts | 300 ++++++++++++++++++ 2 files changed, 308 insertions(+), 12 deletions(-) create mode 100644 src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index 4000f7c54919..5f8254fc2de7 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -58,16 +58,17 @@ export class RequestFeedbackButtonComponent implements OnInit { this.updateParticipation(); } - private updateParticipation(): Observable { + private updateParticipation() { if (this.exercise().id) { - return this.exerciseService.getExerciseDetails(this.exercise().id!).pipe( - map((exerciseResponse: HttpResponse) => { + this.exerciseService.getExerciseDetails(this.exercise().id!).subscribe({ + next: (exerciseResponse: HttpResponse) => { this.participation = this.participationService.getSpecificStudentParticipation(exerciseResponse.body!.exercise.studentParticipations ?? [], false); - return this.participation; - }), - ); + }, + error: (error: HttpErrorResponse) => { + this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); + }, + }); } - return of(undefined); } requestFeedback() { @@ -124,8 +125,3 @@ export class RequestFeedbackButtonComponent implements OnInit { return false; } } - -// tests: athena enabled, athena disabled if I can see the buttons -// check disabled status -// check names, symbols, icons -// click effect diff --git a/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts new file mode 100644 index 000000000000..3fc50106033c --- /dev/null +++ b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts @@ -0,0 +1,300 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { of, Observable } from 'rxjs'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { AlertService } from 'app/core/util/alert.service'; +import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; +import dayjs from 'dayjs/esm'; +import { AssessmentType } from 'app/entities/assessment-type.model'; +import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component'; +import { ArtemisTestModule } from '../../../../test.module'; +import { ExerciseDetailsStudentActionsComponent } from 'app/overview/exercise-details/exercise-details-student-actions.component'; +import { ExerciseActionButtonComponent } from 'app/shared/components/exercise-action-button.component'; +import { CodeButtonComponent } from 'app/shared/components/code-button/code-button.component'; +import { StartPracticeModeButtonComponent } from 'app/shared/components/start-practice-mode-button/start-practice-mode-button.component'; +import { ExtensionPointDirective } from 'app/shared/extension-point/extension-point.directive'; +import { MockRouterLinkDirective } from '../../../../helpers/mocks/directive/mock-router-link.directive'; +import { FeatureToggleDirective } from 'app/shared/feature-toggle/feature-toggle.directive'; +import { MockCourseExerciseService } from '../../../../helpers/mocks/service/mock-course-exercise.service'; +import { Router } from '@angular/router'; +import { MockRouter } from '../../../../helpers/mocks/mock-router'; +import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; +import { MockSyncStorage } from '../../../../helpers/mocks/service/mock-sync-storage.service'; +import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; +import { PasswordComponent } from 'app/account/password/password.component'; +import { MockProfileService } from '../../../../helpers/mocks/service/mock-profile.service'; +import { MockExerciseService } from '../../../../helpers/mocks/service/mock-exercise.service'; + +describe('RequestFeedbackButtonComponent', () => { + let component: RequestFeedbackButtonComponent; + let fixture: ComponentFixture; + let debugElement: DebugElement; + let profileService: ProfileService; + let alertService: AlertService; + let courseExerciseService: CourseExerciseService; + let exerciseService: ExerciseService; + let participationService: ParticipationService; + + beforeEach(() => { + return TestBed.configureTestingModule({ + imports: [ArtemisTestModule, RequestFeedbackButtonComponent], + providers: [{ provide: ProfileService, useClass: MockProfileService }, MockProvider(HttpClient)], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(RequestFeedbackButtonComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + courseExerciseService = debugElement.injector.get(CourseExerciseService); + exerciseService = debugElement.injector.get(ExerciseService); + participationService = debugElement.injector.get(ParticipationService); + profileService = debugElement.injector.get(ProfileService); + alertService = debugElement.injector.get(AlertService); + }); + }); + + function setAthenaEnabled(enabled: boolean) { + jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of({ activeProfiles: enabled ? ['athena'] : [] })); + } + + function mockExerciseDetails(exercise: Exercise) { + jest.spyOn(exerciseService, 'getExerciseDetails').mockReturnValue(of(new HttpResponse({ body: { exercise: exercise } }))); + } + + it('should handle errors when requestFeedback fails', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: true }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.TEXT, course: undefined, studentParticipations: [participation] } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + mockExerciseDetails(exercise); + + jest.spyOn(component, 'assureConditionsSatisfied').mockReturnValue(true); + jest.spyOn(courseExerciseService, 'requestFeedback').mockReturnValue( + new Observable((subscriber) => { + subscriber.error({ error: { errorKey: 'someError' } }); + }), + ); + jest.spyOn(alertService, 'error'); + + component.requestFeedback(); + tick(); + + expect(alertService.error).toHaveBeenCalledWith('artemisApp.exercise.someError'); + })); + + it('should display the button when Athena is enabled and it is not an exam exercise', fakeAsync(() => { + setAthenaEnabled(true); + const exercise = { id: 1, type: ExerciseType.TEXT, course: {} } as Exercise; // course undefined means exam exercise + fixture.componentRef.setInput('exercise', exercise); + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + expect(button.nativeElement.disabled).toBe(true); + })); + + it('should not display the button when it is an exam exercise', fakeAsync(() => { + setAthenaEnabled(true); + fixture.componentRef.setInput('exercise', { id: 1, type: ExerciseType.TEXT, course: undefined } as Exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + const link = debugElement.query(By.css('a')); + expect(button).toBeNull(); + expect(link).toBeNull(); + })); + + it('should disable the button when participation is missing', fakeAsync(() => { + setAthenaEnabled(true); + const exercise = { id: 1, type: ExerciseType.TEXT, course: {}, studentParticipations: undefined } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + expect(button.nativeElement.disabled).toBe(true); + })); + + it('should display the correct button label and style when Athena is enabled', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: true }], + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.TEXT, course: {}, studentParticipations: [participation] } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + component.isExamExercise = false; + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + + const span = button.query(By.css('span')); + expect(span.nativeElement.textContent).toContain('artemisApp.exerciseActions.requestAutomaticFeedback'); + })); + + it('should call requestFeedback() when button is clicked', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: false }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.PROGRAMMING, studentParticipations: [participation], course: {} } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + jest.spyOn(component, 'requestFeedback'); + jest.spyOn(window, 'confirm').mockReturnValue(false); + + const button = debugElement.query(By.css('a')); + button.nativeElement.click(); + tick(); + + expect(component.requestFeedback).toHaveBeenCalled(); + })); + + it('should not proceed when confirmation is cancelled for programming exercise', fakeAsync(() => { + setAthenaEnabled(false); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: false }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.PROGRAMMING, studentParticipations: [participation], course: {} } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + jest.spyOn(component, 'assureConditionsSatisfied').mockReturnValue(true); + jest.spyOn(window, 'confirm').mockReturnValue(false); + jest.spyOn(courseExerciseService, 'requestFeedback'); + + component.requestFeedback(); + tick(); + + expect(window.confirm).toHaveBeenCalled(); + expect(courseExerciseService.requestFeedback).not.toHaveBeenCalled(); + })); + + it('should proceed when confirmation is accepted for programming exercise', fakeAsync(() => { + setAthenaEnabled(false); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: false }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.PROGRAMMING, studentParticipations: [participation], course: {} } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + jest.spyOn(component, 'assureConditionsSatisfied').mockReturnValue(true); + jest.spyOn(window, 'confirm').mockReturnValue(true); + jest.spyOn(courseExerciseService, 'requestFeedback').mockReturnValue(of(participation)); + jest.spyOn(alertService, 'success'); + + component.requestFeedback(); + tick(); + + expect(window.confirm).toHaveBeenCalled(); + expect(courseExerciseService.requestFeedback).toHaveBeenCalledWith(exercise.id); + expect(alertService.success).toHaveBeenCalledWith('artemisApp.exercise.feedbackRequestSent'); + })); + + it('should show an alert when requestFeedback() is called and conditions are not satisfied', fakeAsync(() => { + setAthenaEnabled(true); + + const exercise = { id: 1, type: ExerciseType.TEXT, course: {} } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + + jest.spyOn(component, 'hasAthenaResultForLatestSubmission').mockReturnValue(true); + jest.spyOn(alertService, 'warning'); + + component.requestFeedback(); + + expect(alertService.warning).toHaveBeenCalled(); + })); + + it('should disable the button if latest submission is not submitted or feedback is generating', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: false }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.TEXT, studentParticipations: [participation], course: {} } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + fixture.componentRef.setInput('isGeneratingFeedback', false); + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + expect(button.nativeElement.disabled).toBe(true); + })); + + it('should enable the button if latest submission is submitted and feedback is not generating', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: true }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.TEXT, course: {}, studentParticipations: [participation] } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + fixture.componentRef.setInput('isGeneratingFeedback', false); + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + expect(button.nativeElement.disabled).toBe(false); + })); +}); From d8d0c9bc9bfe566f4cda085fda741506085b21e5 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Mon, 30 Sep 2024 16:09:35 +0200 Subject: [PATCH 10/28] cleanup --- .../request-feedback-button.component.ts | 5 +--- .../request-feedback-button.component.spec.ts | 29 ++++--------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index 5f8254fc2de7..e7b431b499a7 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -1,4 +1,4 @@ -import { Component, input, inject, OnInit, output } from '@angular/core'; +import { Component, inject, input, OnInit, output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; @@ -12,13 +12,10 @@ import { CourseExerciseService } from 'app/exercises/shared/course-exercises/cou import { AssessmentType } from 'app/entities/assessment-type.model'; import { TranslateService } from '@ngx-translate/core'; import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; -import dayjs from 'dayjs/esm'; import { isExamExercise } from 'app/shared/util/utils'; import { ExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; -import { Observable, of } from 'rxjs'; -import { map } from 'rxjs/operators'; @Component({ selector: 'jhi-request-feedback-button', diff --git a/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts index 3fc50106033c..5028669fbdb9 100644 --- a/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts +++ b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts @@ -1,7 +1,5 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; -import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { MockProvider } from 'ng-mocks'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { of, Observable } from 'rxjs'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; @@ -13,26 +11,9 @@ import { CourseExerciseService } from 'app/exercises/shared/course-exercises/cou import { HttpClient, HttpResponse } from '@angular/common/http'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; -import dayjs from 'dayjs/esm'; -import { AssessmentType } from 'app/entities/assessment-type.model'; import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component'; import { ArtemisTestModule } from '../../../../test.module'; -import { ExerciseDetailsStudentActionsComponent } from 'app/overview/exercise-details/exercise-details-student-actions.component'; -import { ExerciseActionButtonComponent } from 'app/shared/components/exercise-action-button.component'; -import { CodeButtonComponent } from 'app/shared/components/code-button/code-button.component'; -import { StartPracticeModeButtonComponent } from 'app/shared/components/start-practice-mode-button/start-practice-mode-button.component'; -import { ExtensionPointDirective } from 'app/shared/extension-point/extension-point.directive'; -import { MockRouterLinkDirective } from '../../../../helpers/mocks/directive/mock-router-link.directive'; -import { FeatureToggleDirective } from 'app/shared/feature-toggle/feature-toggle.directive'; -import { MockCourseExerciseService } from '../../../../helpers/mocks/service/mock-course-exercise.service'; -import { Router } from '@angular/router'; -import { MockRouter } from '../../../../helpers/mocks/mock-router'; -import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; -import { MockSyncStorage } from '../../../../helpers/mocks/service/mock-sync-storage.service'; -import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; -import { PasswordComponent } from 'app/account/password/password.component'; import { MockProfileService } from '../../../../helpers/mocks/service/mock-profile.service'; -import { MockExerciseService } from '../../../../helpers/mocks/service/mock-exercise.service'; describe('RequestFeedbackButtonComponent', () => { let component: RequestFeedbackButtonComponent; @@ -107,7 +88,7 @@ describe('RequestFeedbackButtonComponent', () => { const button = debugElement.query(By.css('button')); expect(button).not.toBeNull(); - expect(button.nativeElement.disabled).toBe(true); + expect(button.nativeElement.disabled).toBeTrue(); })); it('should not display the button when it is an exam exercise', fakeAsync(() => { @@ -136,7 +117,7 @@ describe('RequestFeedbackButtonComponent', () => { const button = debugElement.query(By.css('button')); expect(button).not.toBeNull(); - expect(button.nativeElement.disabled).toBe(true); + expect(button.nativeElement.disabled).toBeTrue(); })); it('should display the correct button label and style when Athena is enabled', fakeAsync(() => { @@ -274,7 +255,7 @@ describe('RequestFeedbackButtonComponent', () => { const button = debugElement.query(By.css('button')); expect(button).not.toBeNull(); - expect(button.nativeElement.disabled).toBe(true); + expect(button.nativeElement.disabled).toBeTrue(); })); it('should enable the button if latest submission is submitted and feedback is not generating', fakeAsync(() => { @@ -295,6 +276,6 @@ describe('RequestFeedbackButtonComponent', () => { const button = debugElement.query(By.css('button')); expect(button).not.toBeNull(); - expect(button.nativeElement.disabled).toBe(false); + expect(button.nativeElement.disabled).toBeFalse(); })); }); From 7bd2101dfbee86d3386fc0b977aefcaa3f969b38 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Mon, 30 Sep 2024 17:06:03 +0200 Subject: [PATCH 11/28] fix test --- .../request-feedback-button.component.ts | 2 +- .../request-feedback-button.component.spec.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index e7b431b499a7..d300c0c4397c 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, input, OnInit, output } from '@angular/core'; +import { Component, OnInit, inject, input, output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; diff --git a/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts index 5028669fbdb9..692d9dc153bc 100644 --- a/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts +++ b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { MockProvider } from 'ng-mocks'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; -import { of, Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { By } from '@angular/platform-browser'; @@ -10,10 +10,10 @@ import { AlertService } from 'app/core/util/alert.service'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component'; import { ArtemisTestModule } from '../../../../test.module'; import { MockProfileService } from '../../../../helpers/mocks/service/mock-profile.service'; +import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; describe('RequestFeedbackButtonComponent', () => { let component: RequestFeedbackButtonComponent; @@ -23,7 +23,6 @@ describe('RequestFeedbackButtonComponent', () => { let alertService: AlertService; let courseExerciseService: CourseExerciseService; let exerciseService: ExerciseService; - let participationService: ParticipationService; beforeEach(() => { return TestBed.configureTestingModule({ @@ -37,14 +36,13 @@ describe('RequestFeedbackButtonComponent', () => { debugElement = fixture.debugElement; courseExerciseService = debugElement.injector.get(CourseExerciseService); exerciseService = debugElement.injector.get(ExerciseService); - participationService = debugElement.injector.get(ParticipationService); profileService = debugElement.injector.get(ProfileService); alertService = debugElement.injector.get(AlertService); }); }); function setAthenaEnabled(enabled: boolean) { - jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of({ activeProfiles: enabled ? ['athena'] : [] })); + jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of({ activeProfiles: enabled ? ['athena'] : [] } as ProfileInfo)); } function mockExerciseDetails(exercise: Exercise) { From 653816d35a58d970ef6a970b332c1c266bd86a94 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Mon, 30 Sep 2024 17:35:27 +0200 Subject: [PATCH 12/28] fix another test --- .../code-editor/code-editor-container.integration.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts index d8f9e0bb8126..7aded7b4350b 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts @@ -75,6 +75,7 @@ import { AlertService } from 'app/core/util/alert.service'; import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; +import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component'; describe('CodeEditorContainerIntegration', () => { let container: CodeEditorContainerComponent; @@ -123,6 +124,7 @@ describe('CodeEditorContainerIntegration', () => { TreeviewItemComponent, MockPipe(ArtemisDatePipe), MockComponent(CodeEditorTutorAssessmentInlineFeedbackComponent), + MockComponent(RequestFeedbackButtonComponent), ], providers: [ ChangeDetectorRef, From dabd8727f16fdf361fca179aeed2e529a92675f1 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Mon, 30 Sep 2024 18:04:34 +0200 Subject: [PATCH 13/28] fix another test --- .../exam/participate/exam-navigation-sidebar.component.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/exam/participate/exam-navigation-sidebar.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exam-navigation-sidebar.component.spec.ts index 45ce49fc2b4d..3a9102c72e8e 100644 --- a/src/test/javascript/spec/component/exam/participate/exam-navigation-sidebar.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exam-navigation-sidebar.component.spec.ts @@ -20,6 +20,7 @@ import { TranslateService } from '@ngx-translate/core'; import { facSaveSuccess, facSaveWarning } from 'src/main/webapp/content/icons/icons'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { ExamLiveEventsButtonComponent } from 'app/exam/participate/events/exam-live-events-button.component'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; describe('ExamNavigationSidebarComponent', () => { let fixture: ComponentFixture; @@ -33,7 +34,7 @@ describe('ExamNavigationSidebarComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ArtemisTestModule, TranslateTestingModule, MockModule(NgbTooltipModule)], + imports: [ArtemisTestModule, TranslateTestingModule, MockModule(NgbTooltipModule), MockModule(ArtemisSharedCommonModule)], declarations: [ExamNavigationSidebarComponent, MockComponent(ExamTimerComponent), MockComponent(ExamLiveEventsButtonComponent)], providers: [ ExamParticipationService, From 41fc7c338d369e3447f7dc29ccb500ec768b2de8 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Tue, 1 Oct 2024 16:34:57 +0200 Subject: [PATCH 14/28] fix missing status icons --- .../app/exercises/shared/result/result.component.html | 6 ++++++ .../request-feedback-button.component.ts | 7 ------- .../participation/ParticipationIntegrationTest.java | 6 ++++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/result/result.component.html b/src/main/webapp/app/exercises/shared/result/result.component.html index f31d9c987090..911320c183df 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.html +++ b/src/main/webapp/app/exercises/shared/result/result.component.html @@ -12,6 +12,9 @@ } @case (ResultTemplateStatus.FEEDBACK_GENERATION_FAILED) { @if (result) { + @if (showIcon) { + + } {{ resultString }} @@ -27,6 +30,9 @@ } @case (ResultTemplateStatus.FEEDBACK_GENERATION_TIMED_OUT) { @if (result) { + @if (showIcon) { + + } {{ resultString }} diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index d300c0c4397c..4457996c9a1d 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -73,13 +73,6 @@ export class RequestFeedbackButtonComponent implements OnInit { return; } - if (this.exercise().type === ExerciseType.PROGRAMMING) { - const confirmLockRepository = this.translateService.instant('artemisApp.exercise.lockRepositoryWarning'); - if (!window.confirm(confirmLockRepository)) { - return; - } - } - this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ next: (participation: StudentParticipation) => { if (participation) { diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java index 6fa5a1146440..87a8d68af1d8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java @@ -537,9 +537,11 @@ void requestFeedbackAlreadySent() throws Exception { HttpStatus.BAD_REQUEST); } + // todo due date, already sent, all server side cases + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void requestFeedbackSuccess_withAthenaSuccess() throws Exception { + void requestProgrammingFeedbackSuccess_withAthenaSuccess() throws Exception { var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); course.setRestrictedAthenaModulesAccess(true); @@ -656,7 +658,7 @@ void requestModelingFeedbackSuccess_withAthenaSuccess() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void requestFeedbackSuccess_withAthenaFailure() throws Exception { + void requestProgrammingFeedbackSuccess_withAthenaFailure() throws Exception { var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); course.setRestrictedAthenaModulesAccess(true); From 1f8517ba9ea35ca790d3ca53beae6b918f6e54d4 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Wed, 2 Oct 2024 16:01:35 +0200 Subject: [PATCH 15/28] fix results view for the dashboard --- .../java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java index 7503427a81fc..5ace9792fc75 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java @@ -562,7 +562,7 @@ public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Particip boolean ratedOrPractice = Boolean.TRUE.equals(result.isRated()) || participation.isPracticeMode(); boolean noProgrammingAndAssessmentOver = !isProgrammingExercise && isAssessmentOver; // For programming exercises we check that the assessment due date has passed (if set) for manual results otherwise we always show the automatic result - boolean programmingAfterAssessmentOrAutomatic = isProgrammingExercise && ((result.isManual() && isAssessmentOver) || result.isAutomatic()); + boolean programmingAfterAssessmentOrAutomatic = isProgrammingExercise && ((result.isManual() && isAssessmentOver) || result.isAutomatic() || result.isAthenaAutomatic()); if (ratedOrPractice && (noProgrammingAndAssessmentOver || programmingAfterAssessmentOrAutomatic)) { // take the first found result that fulfills the above requirements // or From babb3ae66d6650b470b772e069e0727e23bc9738 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Wed, 2 Oct 2024 16:02:02 +0200 Subject: [PATCH 16/28] reformat --- .../java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java index 5ace9792fc75..e8b63161f4cd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java @@ -562,7 +562,8 @@ public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Particip boolean ratedOrPractice = Boolean.TRUE.equals(result.isRated()) || participation.isPracticeMode(); boolean noProgrammingAndAssessmentOver = !isProgrammingExercise && isAssessmentOver; // For programming exercises we check that the assessment due date has passed (if set) for manual results otherwise we always show the automatic result - boolean programmingAfterAssessmentOrAutomatic = isProgrammingExercise && ((result.isManual() && isAssessmentOver) || result.isAutomatic() || result.isAthenaAutomatic()); + boolean programmingAfterAssessmentOrAutomatic = isProgrammingExercise + && ((result.isManual() && isAssessmentOver) || result.isAutomatic() || result.isAthenaAutomatic()); if (ratedOrPractice && (noProgrammingAndAssessmentOver || programmingAfterAssessmentOrAutomatic)) { // take the first found result that fulfills the above requirements // or From adad3694c134516f71cd606d6940bad266968ce7 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Wed, 2 Oct 2024 19:01:34 +0200 Subject: [PATCH 17/28] adjust tests --- .../utils/programming-exercise.utils.ts | 5 +- .../shared/result/result.component.ts | 9 +-- .../result/updating-result.component.ts | 4 +- .../course-exercise-details.component.ts | 2 +- .../component/exercises/shared/result.spec.ts | 26 +++++++- .../request-feedback-button.component.spec.ts | 63 ++----------------- .../spec/component/utils/result.utils.spec.ts | 16 +++-- .../spec/service/result.service.spec.ts | 6 +- 8 files changed, 54 insertions(+), 77 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts b/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts index 4e99d717f019..262fcb9e30a7 100644 --- a/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts +++ b/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts @@ -7,7 +7,7 @@ import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { isPracticeMode } from 'app/entities/participation/student-participation.model'; -import { isAIResultAndProcessed } from 'app/exercises/shared/result/result.utils'; +import { isAIResultAndFailed, isAIResultAndIsBeingProcessed, isAIResultAndProcessed, isAIResultAndTimedOut } from 'app/exercises/shared/result/result.utils'; export const createBuildPlanUrl = (template: string, projectKey: string, buildPlanId: string): string | undefined => { if (template && projectKey && buildPlanId) { @@ -63,6 +63,9 @@ export const isResultPreliminary = (latestResult: Result, programmingExercise?: if (isAIResultAndProcessed(latestResult)) { return true; } + if (isAIResultAndIsBeingProcessed(latestResult) || isAIResultAndTimedOut(latestResult) || isAIResultAndFailed(latestResult)) { + return false; + } if (latestResult.participation?.type === ParticipationType.PROGRAMMING && isPracticeMode(latestResult.participation)) { return false; } diff --git a/src/main/webapp/app/exercises/shared/result/result.component.ts b/src/main/webapp/app/exercises/shared/result/result.component.ts index c26bb427fa68..3415021e11c7 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/result.component.ts @@ -1,13 +1,6 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, Optional, SimpleChanges } from '@angular/core'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; -import { - MissingResultInformation, - ResultTemplateStatus, - evaluateTemplateStatus, - getResultIconClass, - getTextColorClass, - isAIResultAndFailed, -} from 'app/exercises/shared/result/result.utils'; +import { MissingResultInformation, ResultTemplateStatus, evaluateTemplateStatus, getResultIconClass, getTextColorClass } from 'app/exercises/shared/result/result.utils'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; diff --git a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts index f64acf1cced0..bee34377e6f8 100644 --- a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts @@ -13,7 +13,7 @@ import { StudentParticipation } from 'app/entities/participation/student-partici import { Result } from 'app/entities/result.model'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { getLatestResultOfStudentParticipation, hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; -import { isAIResultAndIsBeingProcessed, MissingResultInformation } from 'app/exercises/shared/result/result.utils'; +import { MissingResultInformation, isAIResultAndIsBeingProcessed } from 'app/exercises/shared/result/result.utils'; import { convertDateFromServer } from 'app/utils/date.utils'; /** @@ -107,6 +107,8 @@ export class UpdatingResultComponent implements OnChanges, OnDestroy { tap((result) => { if ((Result.isAthenaAIResult(result) && isAIResultAndIsBeingProcessed(result)) || result.rated) { this.result = result; + } else if (result.rated === false && this.showUngradedResults) { + this.result = result; } else { this.result = getLatestResultOfStudentParticipation(this.participation, this.showUngradedResults, false); } diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index 8a3038bad905..4790d0b24bf9 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -1,7 +1,7 @@ import { Component, ContentChild, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; -import { combineLatest, Subscription } from 'rxjs'; +import { Subscription, combineLatest } from 'rxjs'; import { filter, skip } from 'rxjs/operators'; import { Result } from 'app/entities/result.model'; import dayjs from 'dayjs/esm'; diff --git a/src/test/javascript/spec/component/exercises/shared/result.spec.ts b/src/test/javascript/spec/component/exercises/shared/result.spec.ts index 72c2cd1aafca..ff076e80d8c8 100644 --- a/src/test/javascript/spec/component/exercises/shared/result.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/result.spec.ts @@ -12,7 +12,7 @@ import { TranslateService } from '@ngx-translate/core'; import { cloneDeep } from 'lodash-es'; import { Submission } from 'app/entities/submission.model'; import { ExerciseType } from 'app/entities/exercise.model'; -import { faQuestionCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; +import { faCheckCircle, faQuestionCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { ModelingExercise } from 'app/entities/modeling-exercise.model'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; @@ -141,11 +141,31 @@ describe('ResultComponent', () => { expect(component.result!.participation).toEqual(participation1); expect(component.submission).toEqual(submission1); expect(component.textColorClass).toBe('text-secondary'); - expect(component.resultIconClass).toEqual(faQuestionCircle); + expect(component.resultIconClass).toEqual(faCheckCircle); expect(component.resultString).toBe('artemisApp.result.resultString.short (artemisApp.result.preliminary)'); expect(component.templateStatus).toBe(ResultTemplateStatus.HAS_RESULT); }); + it('should set (automatic athena) results for programming exercise', () => { + const submission1: Submission = { id: 1 }; + const result1: Result = { id: 1, submission: submission1, score: 0.8, assessmentType: AssessmentType.AUTOMATIC_ATHENA, successful: true }; + const result2: Result = { id: 2 }; + const participation1 = cloneDeep(programmingParticipation); + participation1.results = [result1, result2]; + component.participation = participation1; + component.showUngradedResults = true; + + fixture.detectChanges(); + + expect(component.result).toEqual(result1); + expect(component.result!.participation).toEqual(participation1); + expect(component.submission).toEqual(submission1); + expect(component.textColorClass).toBe('text-secondary'); + expect(component.resultIconClass).toEqual(faCheckCircle); + expect(component.resultString).toBe('artemisApp.result.resultString.automaticAIFeedbackSuccessful (artemisApp.result.preliminary)'); + expect(component.templateStatus).toBe(ResultTemplateStatus.HAS_RESULT); + }); + it('should set (automatic athena) results for text exercise', () => { const submission1: Submission = { id: 1 }; const result1: Result = { id: 1, submission: submission1, score: 1, assessmentType: AssessmentType.AUTOMATIC_ATHENA, successful: true }; @@ -161,7 +181,7 @@ describe('ResultComponent', () => { expect(component.result!.participation).toEqual(participation1); expect(component.submission).toEqual(submission1); expect(component.textColorClass).toBe('text-secondary'); - expect(component.resultIconClass).toEqual(faQuestionCircle); + expect(component.resultIconClass).toEqual(faCheckCircle); expect(component.resultString).toBe('artemisApp.result.resultString.short (artemisApp.result.preliminary)'); }); diff --git a/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts index 692d9dc153bc..94219fdd2420 100644 --- a/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts +++ b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts @@ -60,7 +60,6 @@ describe('RequestFeedbackButtonComponent', () => { fixture.componentRef.setInput('exercise', exercise); mockExerciseDetails(exercise); - jest.spyOn(component, 'assureConditionsSatisfied').mockReturnValue(true); jest.spyOn(courseExerciseService, 'requestFeedback').mockReturnValue( new Observable((subscriber) => { subscriber.error({ error: { errorKey: 'someError' } }); @@ -157,7 +156,12 @@ describe('RequestFeedbackButtonComponent', () => { fixture.detectChanges(); jest.spyOn(component, 'requestFeedback'); - jest.spyOn(window, 'confirm').mockReturnValue(false); + jest.spyOn(courseExerciseService, 'requestFeedback').mockReturnValue( + new Observable((subscriber) => { + subscriber.next(); + subscriber.complete(); + }), + ); const button = debugElement.query(By.css('a')); button.nativeElement.click(); @@ -166,61 +170,6 @@ describe('RequestFeedbackButtonComponent', () => { expect(component.requestFeedback).toHaveBeenCalled(); })); - it('should not proceed when confirmation is cancelled for programming exercise', fakeAsync(() => { - setAthenaEnabled(false); - const participation = { - id: 1, - submissions: [{ id: 1, submitted: false }], - testRun: false, - } as StudentParticipation; - const exercise = { id: 1, type: ExerciseType.PROGRAMMING, studentParticipations: [participation], course: {} } as Exercise; - fixture.componentRef.setInput('exercise', exercise); - mockExerciseDetails(exercise); - - component.ngOnInit(); - tick(); - fixture.detectChanges(); - - jest.spyOn(component, 'assureConditionsSatisfied').mockReturnValue(true); - jest.spyOn(window, 'confirm').mockReturnValue(false); - jest.spyOn(courseExerciseService, 'requestFeedback'); - - component.requestFeedback(); - tick(); - - expect(window.confirm).toHaveBeenCalled(); - expect(courseExerciseService.requestFeedback).not.toHaveBeenCalled(); - })); - - it('should proceed when confirmation is accepted for programming exercise', fakeAsync(() => { - setAthenaEnabled(false); - const participation = { - id: 1, - submissions: [{ id: 1, submitted: false }], - testRun: false, - } as StudentParticipation; - const exercise = { id: 1, type: ExerciseType.PROGRAMMING, studentParticipations: [participation], course: {} } as Exercise; - fixture.componentRef.setInput('exercise', exercise); - - mockExerciseDetails(exercise); - - component.ngOnInit(); - tick(); - fixture.detectChanges(); - - jest.spyOn(component, 'assureConditionsSatisfied').mockReturnValue(true); - jest.spyOn(window, 'confirm').mockReturnValue(true); - jest.spyOn(courseExerciseService, 'requestFeedback').mockReturnValue(of(participation)); - jest.spyOn(alertService, 'success'); - - component.requestFeedback(); - tick(); - - expect(window.confirm).toHaveBeenCalled(); - expect(courseExerciseService.requestFeedback).toHaveBeenCalledWith(exercise.id); - expect(alertService.success).toHaveBeenCalledWith('artemisApp.exercise.feedbackRequestSent'); - })); - it('should show an alert when requestFeedback() is called and conditions are not satisfied', fakeAsync(() => { setAthenaEnabled(true); diff --git a/src/test/javascript/spec/component/utils/result.utils.spec.ts b/src/test/javascript/spec/component/utils/result.utils.spec.ts index 4796766de234..24303c3e59af 100644 --- a/src/test/javascript/spec/component/utils/result.utils.spec.ts +++ b/src/test/javascript/spec/component/utils/result.utils.spec.ts @@ -15,6 +15,7 @@ import { faCheckCircle, faQuestionCircle, faTimesCircle } from '@fortawesome/fre import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'; import { ExerciseType } from 'app/entities/exercise.model'; import { Result } from 'app/entities/result.model'; +import dayjs from 'dayjs/esm'; describe('ResultUtils', () => { it('should filter out all non unreferenced feedbacks', () => { @@ -69,7 +70,7 @@ describe('ResultUtils', () => { { result: { score: 0, successful: undefined, assessmentType: AssessmentType.AUTOMATIC_ATHENA }, templateStatus: ResultTemplateStatus.IS_GENERATING_FEEDBACK, - expected: 'text-primary', + expected: 'text-secondary', }, { result: { score: 0, successful: true, assessmentType: AssessmentType.AUTOMATIC_ATHENA }, @@ -128,7 +129,12 @@ describe('ResultUtils', () => { expected: faTimesCircle, }, { - result: { feedbacks: [{ type: FeedbackType.AUTOMATIC, text: 'AI result being generated test case' }], assessmentType: AssessmentType.AUTOMATIC_ATHENA }, + result: { + feedbacks: [{ type: FeedbackType.AUTOMATIC, text: 'AI result being generated test case' }], + assessmentType: AssessmentType.AUTOMATIC_ATHENA, + successful: undefined, + completionDate: dayjs().add(5, 'minutes'), + }, templateStatus: ResultTemplateStatus.IS_GENERATING_FEEDBACK, expected: faCircleNotch, }, @@ -138,9 +144,10 @@ describe('ResultUtils', () => { participation: { type: ParticipationType.STUDENT, exercise: { type: ExerciseType.TEXT } }, successful: true, assessmentType: AssessmentType.AUTOMATIC_ATHENA, + completionDate: dayjs().subtract(5, 'minutes'), } as Result, templateStatus: ResultTemplateStatus.HAS_RESULT, - expected: faQuestionCircle, + expected: faCheckCircle, }, { result: { @@ -148,9 +155,10 @@ describe('ResultUtils', () => { participation: { type: ParticipationType.STUDENT, exercise: { type: ExerciseType.TEXT } }, successful: false, assessmentType: AssessmentType.AUTOMATIC_ATHENA, + completionDate: dayjs().subtract(5, 'minutes'), } as Result, templateStatus: ResultTemplateStatus.HAS_RESULT, - expected: faQuestionCircle, + expected: faTimesCircle, }, ])('should correctly determine result icon', ({ result, templateStatus, expected }) => { expect(getResultIconClass(result, templateStatus!)).toBe(expected); diff --git a/src/test/javascript/spec/service/result.service.spec.ts b/src/test/javascript/spec/service/result.service.spec.ts index 8afb25f538cd..abd14927502c 100644 --- a/src/test/javascript/spec/service/result.service.spec.ts +++ b/src/test/javascript/spec/service/result.service.spec.ts @@ -305,8 +305,10 @@ describe('ResultService', () => { it('should return correct string for Athena non graded successful feedback', () => { programmingExercise.assessmentDueDate = dayjs().subtract(5, 'minutes'); - expect(resultService.getResultString(result6, programmingExercise)).toBe('artemisApp.result.resultString.automaticAIFeedbackSuccessful'); - expect(translateServiceSpy).toHaveBeenCalledOnce(); + expect(resultService.getResultString(result6, programmingExercise)).toBe( + 'artemisApp.result.resultString.automaticAIFeedbackSuccessful (artemisApp.result.preliminary)', + ); + expect(translateServiceSpy).toHaveBeenCalledTimes(2); }); it('should return correct string for Athena non graded unsuccessful feedback', () => { From 21a6d888da680e24b52a1e4a7b8baef09f15c227 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Wed, 2 Oct 2024 19:20:59 +0200 Subject: [PATCH 18/28] adjust server tests --- .../SingleUserNotificationServiceTest.java | 2 + .../ParticipationIntegrationTest.java | 89 +++++++++---------- .../ModelingExerciseIntegrationTest.java | 2 + ...ExerciseLocalVCLocalCIIntegrationTest.java | 2 + .../localvcci/LocalCIIntegrationTest.java | 2 + .../LocalVCLocalCIIntegrationTest.java | 2 + 6 files changed, 53 insertions(+), 46 deletions(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java index 9faaf7d03bc3..d91a4095ed83 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java @@ -58,6 +58,7 @@ import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -105,6 +106,7 @@ import de.tum.cit.aet.artemis.text.util.TextExerciseFactory; import de.tum.cit.aet.artemis.tutorialgroup.domain.TutorialGroup; +@Disabled class SingleUserNotificationServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "singleusernotification"; diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java index 87a8d68af1d8..66be8104ac5b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java @@ -3,7 +3,6 @@ import static de.tum.cit.aet.artemis.core.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; import static de.tum.cit.aet.artemis.core.util.TestResourceUtils.HalfSecond; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.doNothing; @@ -24,7 +23,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; @@ -537,7 +535,49 @@ void requestFeedbackAlreadySent() throws Exception { HttpStatus.BAD_REQUEST); } - // todo due date, already sent, all server side cases + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void requestProgrammingFeedbackIfARequestAlreadySent_withAthenaSuccess() throws Exception { + + var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); + course.setRestrictedAthenaModulesAccess(true); + this.courseRepository.save(course); + + this.programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); + this.exerciseRepository.save(programmingExercise); + + athenaRequestMockProvider.mockGetFeedbackSuggestionsAndExpect("programming"); + + var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, + userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + + var localRepo = new LocalRepository(defaultBranch); + localRepo.configureRepos("testLocalRepo", "testOriginRepo"); + + participation.setRepositoryUri(ParticipationFactory.getMockFileRepositoryUri(localRepo).getURI().toString()); + participationRepo.save(participation); + + gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); + + Result result1 = participationUtilService.createSubmissionAndResult(participation, 100, false); + Result result2 = participationUtilService.addResultToParticipation(participation, result1.getSubmission()); + result2.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); + result2.setSuccessful(null); + resultRepository.save(result2); + + request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, HttpStatus.OK); + + verify(programmingMessagingService, timeout(2000).times(2)).notifyUserAboutNewResult(resultCaptor.capture(), any()); + + Result invokedResult = resultCaptor.getAllValues().getFirst(); + assertThat(invokedResult).isNotNull(); + assertThat(invokedResult.getId()).isNotNull(); + assertThat(invokedResult.isSuccessful()).isTrue(); + assertThat(invokedResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedResult.getFeedbacks()).hasSize(1); + + localRepo.resetLocalRepo(); + } @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") @@ -569,9 +609,6 @@ void requestProgrammingFeedbackSuccess_withAthenaSuccess() throws Exception { result2.setCompletionDate(ZonedDateTime.now()); resultRepository.save(result2); - doNothing().when(programmingExerciseParticipationService).lockStudentRepositoryAndParticipation(eq(programmingExercise), any()); - doNothing().when(programmingExerciseParticipationService).unlockStudentRepositoryAndParticipation(any()); - request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, HttpStatus.OK); verify(programmingMessagingService, timeout(2000).times(2)).notifyUserAboutNewResult(resultCaptor.capture(), any()); @@ -685,9 +722,6 @@ void requestProgrammingFeedbackSuccess_withAthenaFailure() throws Exception { result2.setCompletionDate(ZonedDateTime.now()); resultRepository.save(result2); - doNothing().when(programmingExerciseParticipationService).lockStudentRepositoryAndParticipation(any(), any()); - doNothing().when(programmingExerciseParticipationService).unlockStudentRepositoryAndParticipation(any()); - request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, HttpStatus.OK); verify(programmingMessagingService, timeout(2000).times(2)).notifyUserAboutNewResult(resultCaptor.capture(), any()); @@ -1658,43 +1692,6 @@ void whenFeedbackRequestedAndRateLimitExceeded_thenFail() throws Exception { localRepo.resetLocalRepo(); } - @Disabled // will be re-enabled in https://github.com/ls1intum/Artemis/pull/9324/ - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void whenFeedbackRequestedAndRateLimitStillUnknownDueRequestsInProgress_thenFail() throws Exception { - - programmingExercise.setDueDate(ZonedDateTime.now().plusDays(100)); - programmingExercise = exerciseRepository.save(programmingExercise); - - var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, - userUtilService.getUserByLogin(TEST_PREFIX + "student1")); - - var localRepo = new LocalRepository(defaultBranch); - localRepo.configureRepos("testLocalRepo", "testOriginRepo"); - - participation.setRepositoryUri(ParticipationFactory.getMockFileRepositoryUri(localRepo).getURI().toString()); - participationRepo.save(participation); - - gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); - - var result = ParticipationFactory.generateResult(false, 100).participation(participation); - result.setCompletionDate(ZonedDateTime.now()); - resultRepository.save(result); - - // generate 5 athena results - for (int i = 0; i < 5; i++) { - var athenaResult = ParticipationFactory.generateResult(false, 100).participation(participation); - athenaResult.setCompletionDate(ZonedDateTime.now()); - athenaResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); - athenaResult.setSuccessful(null); - resultRepository.save(athenaResult); - } - - request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); - - localRepo.resetLocalRepo(); - } - @Nested @Isolated class ParticipationIntegrationIsolatedTest { diff --git a/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java index 95087f641584..ff65a5c27c1f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java @@ -19,6 +19,7 @@ import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -74,6 +75,7 @@ import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.cit.aet.artemis.tutorialgroup.domain.TutorParticipationStatus; +@Disabled class ModelingExerciseIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "modelingexerciseintegration"; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java index 16c1077efd2f..f7905c80d75d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -48,6 +49,7 @@ import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; +@Disabled @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ProgrammingExerciseLocalVCLocalCIIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalCIIntegrationTest.java index 70d024bab294..3a526be513f5 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalCIIntegrationTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.mockito.ArgumentMatcher; @@ -66,6 +67,7 @@ import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; import de.tum.cit.aet.artemis.programming.util.LocalRepository; +@Disabled @TestInstance(TestInstance.Lifecycle.PER_CLASS) class LocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalVCLocalCIIntegrationTest.java index c171a3f60cca..cc5e089bcd97 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalVCLocalCIIntegrationTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.slf4j.Logger; @@ -61,6 +62,7 @@ * This class contains integration tests for the base repositories (template, solution, tests) and the different types of assignment repositories (student assignment, teaching * assistant assignment, instructor assignment). */ +@Disabled @TestInstance(TestInstance.Lifecycle.PER_CLASS) class LocalVCLocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { From da44202d97b7fe04c5606ba29fa500097b6be1b7 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Wed, 2 Oct 2024 20:13:15 +0200 Subject: [PATCH 19/28] adjust server tests --- .../notification/SingleUserNotificationServiceTest.java | 2 -- .../aet/artemis/modeling/ModelingExerciseIntegrationTest.java | 2 -- .../ProgrammingExerciseLocalVCLocalCIIntegrationTest.java | 2 -- .../artemis/programming/localvcci/LocalCIIntegrationTest.java | 2 -- .../programming/localvcci/LocalVCLocalCIIntegrationTest.java | 2 -- 5 files changed, 10 deletions(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java index d91a4095ed83..9faaf7d03bc3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java @@ -58,7 +58,6 @@ import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -106,7 +105,6 @@ import de.tum.cit.aet.artemis.text.util.TextExerciseFactory; import de.tum.cit.aet.artemis.tutorialgroup.domain.TutorialGroup; -@Disabled class SingleUserNotificationServiceTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "singleusernotification"; diff --git a/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java index ff65a5c27c1f..95087f641584 100644 --- a/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/modeling/ModelingExerciseIntegrationTest.java @@ -19,7 +19,6 @@ import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -75,7 +74,6 @@ import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; import de.tum.cit.aet.artemis.tutorialgroup.domain.TutorParticipationStatus; -@Disabled class ModelingExerciseIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "modelingexerciseintegration"; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java index f7905c80d75d..16c1077efd2f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java @@ -21,7 +21,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -49,7 +48,6 @@ import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; -@Disabled @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ProgrammingExerciseLocalVCLocalCIIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalCIIntegrationTest.java index 3a526be513f5..70d024bab294 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalCIIntegrationTest.java @@ -34,7 +34,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.mockito.ArgumentMatcher; @@ -67,7 +66,6 @@ import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; import de.tum.cit.aet.artemis.programming.util.LocalRepository; -@Disabled @TestInstance(TestInstance.Lifecycle.PER_CLASS) class LocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalVCLocalCIIntegrationTest.java index cc5e089bcd97..c171a3f60cca 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/localvcci/LocalVCLocalCIIntegrationTest.java @@ -32,7 +32,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.slf4j.Logger; @@ -62,7 +61,6 @@ * This class contains integration tests for the base repositories (template, solution, tests) and the different types of assignment repositories (student assignment, teaching * assistant assignment, instructor assignment). */ -@Disabled @TestInstance(TestInstance.Lifecycle.PER_CLASS) class LocalVCLocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { From 8ba29dd395aec03faca55f78f1b86442f1daa409 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Wed, 2 Oct 2024 21:07:59 +0200 Subject: [PATCH 20/28] add value for the number of feedback requests --- .../ProgrammingExerciseCodeReviewFeedbackService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 398b3a579eeb..49641d18531f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -12,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -60,6 +61,9 @@ public class ProgrammingExerciseCodeReviewFeedbackService { private final ProgrammingMessagingService programmingMessagingService; + @Value("${artemis.athena.allowed-feedback-attempts:20}") + private int allowedFeedbackAttempts; + public ProgrammingExerciseCodeReviewFeedbackService(GroupNotificationService groupNotificationService, Optional athenaFeedbackSuggestionsService, SubmissionService submissionService, ResultService resultService, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ResultRepository resultRepository, @@ -224,7 +228,7 @@ private void checkRateLimitOrThrow(ProgrammingExerciseStudentParticipation parti long countOfSuccessfulRequests = athenaResults.stream().filter(result -> result.isSuccessful() == Boolean.TRUE).count(); - if (countOfSuccessfulRequests >= 20) { + if (countOfSuccessfulRequests >= this.allowedFeedbackAttempts) { throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); } } From 5b9ec971bcac083adcd5e22efc5bea6451ddf58c Mon Sep 17 00:00:00 2001 From: Dmytro Polityka <33299157+undernagruzez@users.noreply.github.com> Date: Thu, 3 Oct 2024 01:28:49 +0200 Subject: [PATCH 21/28] Update src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html Co-authored-by: Julian Christl --- .../code-editor/actions/code-editor-actions.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html b/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html index 7737158d8ce5..d0186a25665e 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html @@ -1,5 +1,5 @@ @if (!!participation()?.exercise) { - + } @if (commitState === CommitState.CONFLICT) {
From f83e5d02343a4ba2daf7982dc6ce682d515ded41 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka <33299157+undernagruzez@users.noreply.github.com> Date: Thu, 3 Oct 2024 01:29:02 +0200 Subject: [PATCH 22/28] Update src/main/webapp/i18n/en/result.json Co-authored-by: Julian Christl --- src/main/webapp/i18n/en/result.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/en/result.json b/src/main/webapp/i18n/en/result.json index 6f5047025659..f8178c0cde1e 100644 --- a/src/main/webapp/i18n/en/result.json +++ b/src/main/webapp/i18n/en/result.json @@ -92,7 +92,7 @@ "preliminary": "preliminary", "preliminaryTooltip": "Your result is not final yet, because more tests will be executed after the due date", "preliminaryTooltipSemiAutomatic": "Your result is not final yet, because more tests will be executed after the due date or a manual assessment will be done.", - "preliminaryTooltipAthena": "This is a grading by the AI, the actual result may differ", + "preliminaryTooltipAthena": "This is an AI grading. The actual result may differ", "codeIssuesTooltip": "The automatic code analysis generated some warnings for your code.", "noResultDetails": "No result details available.", "onlyCompilationTested": "Your code compiled successfully. There are currently no tests visible.", From bf32bc6f23f91eaaa737156c36fbef61e7983797 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka <33299157+undernagruzez@users.noreply.github.com> Date: Thu, 3 Oct 2024 01:29:16 +0200 Subject: [PATCH 23/28] Update src/main/webapp/i18n/de/result.json Co-authored-by: Julian Christl --- src/main/webapp/i18n/de/result.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/de/result.json b/src/main/webapp/i18n/de/result.json index 99a36a0d4057..28e90295cbea 100644 --- a/src/main/webapp/i18n/de/result.json +++ b/src/main/webapp/i18n/de/result.json @@ -92,7 +92,7 @@ "preliminary": "vorläufig", "preliminaryTooltip": "Dein Ergebnis ist noch nicht endgültig, weil weitere Tests nach der Einreichungsfrist ausgeführt werden.", "preliminaryTooltipSemiAutomatic": "Dein Ergebnis ist noch nicht endgültig, weil weitere Tests nach der Einreichungsfrist ausgeführt werden oder eine manuelle Bewertung aussteht.", - "preliminaryTooltipAthena": "Dies ist eine Bewertung durch die KI, das tatsächliche Ergebnis kann abweichen.", + "preliminaryTooltipAthena": "Dies ist eine KI-Bewertung. Das tatsächliche Ergebnis kann abweichen.", "codeIssuesTooltip": "Die automatische Codeanalyse hat Codeissues gefunden.", "noResultDetails": "Keine weiteren Informationen verfügbar für dieses Ergebnis.", "onlyCompilationTested": "Dein Code kompiliert erfolgreich. Derzeit sind keine Testfälle sichtbar.", From f4a24491a7874aa4e935ccc07cb4e0d55d71d34d Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Thu, 3 Oct 2024 03:27:22 +0200 Subject: [PATCH 24/28] comments of Julian --- .../aet/artemis/exercise/domain/Exercise.java | 4 +-- .../exercise/web/ParticipationResource.java | 5 ++-- ...mingExerciseCodeReviewFeedbackService.java | 7 +++--- src/main/webapp/app/entities/result.model.ts | 18 ------------- ...code-editor-student-container.component.ts | 3 ++- ...exercise-trigger-build-button.component.ts | 5 ++-- .../assessment-progress-label.component.ts | 3 ++- ...exercise-assessment-dashboard.component.ts | 3 ++- .../exercise-scores.component.ts | 3 ++- .../shared/feedback/feedback.component.html | 6 ++--- .../participation/participation.utils.ts | 3 ++- .../shared/result/result.component.ts | 13 +++++++--- .../exercises/shared/result/result.service.ts | 7 +++--- .../exercises/shared/result/result.utils.ts | 25 +++++++++++++------ .../result/updating-result.component.ts | 6 ++--- ...rcise-details-student-actions.component.ts | 8 +++--- .../request-feedback-button.component.ts | 8 +++--- 17 files changed, 66 insertions(+), 61 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java index e8b63161f4cd..3c3f1f26a239 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java @@ -562,9 +562,9 @@ public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Particip boolean ratedOrPractice = Boolean.TRUE.equals(result.isRated()) || participation.isPracticeMode(); boolean noProgrammingAndAssessmentOver = !isProgrammingExercise && isAssessmentOver; // For programming exercises we check that the assessment due date has passed (if set) for manual results otherwise we always show the automatic result - boolean programmingAfterAssessmentOrAutomatic = isProgrammingExercise + boolean programmingAfterAssessmentOrAutomaticOrAthena = isProgrammingExercise && ((result.isManual() && isAssessmentOver) || result.isAutomatic() || result.isAthenaAutomatic()); - if (ratedOrPractice && (noProgrammingAndAssessmentOver || programmingAfterAssessmentOrAutomatic)) { + if (ratedOrPractice && (noProgrammingAndAssessmentOver || programmingAfterAssessmentOrAutomaticOrAthena)) { // take the first found result that fulfills the above requirements // or // take newer results and thus disregard older ones 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 7c15146e5592..5fdadb527f4c 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 @@ -392,7 +392,7 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc StudentParticipation participation = (exercise instanceof ProgrammingExercise) ? programmingExerciseParticipationService.findStudentParticipationByExerciseAndStudentId(exercise, principal.getName()) : studentParticipationRepository.findByExerciseIdAndStudentLogin(exercise.getId(), principal.getName()) - .orElseThrow(() -> new BadRequestAlertException("Participation not found", "participation", "noSubmissionExists", true)); + .orElseThrow(() -> new BadRequestAlertException("Submission not found", "participation", "noSubmissionExists", true)); checkAccessPermissionOwner(participation, user); participation = studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId()); @@ -410,9 +410,8 @@ else if (exercise instanceof ProgrammingExercise) { } // Check if feedback has already been requested - var currentDate = now(); var latestResult = participation.findLatestResult(); - if (latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA && latestResult.getCompletionDate().isAfter(now())) { + if (latestResult != null && latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA && latestResult.getCompletionDate().isAfter(now())) { throw new BadRequestAlertException("Request has already been sent", "participation", "feedbackRequestAlreadySent", true); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 49641d18531f..935a3412b10e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -116,14 +116,14 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici var submissionOptional = programmingExerciseParticipationService.findProgrammingExerciseParticipationWithLatestSubmissionAndResult(participation.getId()) .findLatestSubmission(); if (submissionOptional.isEmpty()) { - throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionÊxists"); + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionExists"); } var submission = submissionOptional.get(); // save result and transmit it over websockets to notify the client about the status var automaticResult = this.submissionService.saveNewEmptyResult(submission); automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); - automaticResult.setRated(true); + automaticResult.setRated(true); // we want to use this feedback to give the grade in the future automaticResult.setScore(100.0); automaticResult.setSuccessful(null); automaticResult.setCompletionDate(ZonedDateTime.now().plusMinutes(5)); // we do not want to show dates without a completion date, but we want the students to know their @@ -164,7 +164,8 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici feedback.setType(FeedbackType.AUTOMATIC); feedback.setCredits(individualFeedbackItem.credits()); return feedback; - }).sorted(Comparator.comparing(Feedback::getCredits)).toList(); + }).sorted(Comparator.comparing(Feedback::getCredits, Comparator.nullsLast(Comparator.naturalOrder()))).toList(); + ; automaticResult.setSuccessful(true); automaticResult.setCompletionDate(ZonedDateTime.now()); diff --git a/src/main/webapp/app/entities/result.model.ts b/src/main/webapp/app/entities/result.model.ts index d6c2f96adaaa..47fff80fda31 100644 --- a/src/main/webapp/app/entities/result.model.ts +++ b/src/main/webapp/app/entities/result.model.ts @@ -39,24 +39,6 @@ export class Result implements BaseEntity { this.successful = false; // default value } - /** - * Checks whether the result is a manual result. A manual result can be from type MANUAL or SEMI_AUTOMATIC - * - * @return true if the result is a manual result - */ - public static isManualResult(that: Result): boolean { - return that.assessmentType === AssessmentType.MANUAL || that.assessmentType === AssessmentType.SEMI_AUTOMATIC; - } - - /** - * Checks whether the result is generated by Athena AI. - * - * @return true if the result is an automatic Athena AI result - */ - public static isAthenaAIResult(that: Result): boolean { - return that.assessmentType === AssessmentType.AUTOMATIC_ATHENA; - } - /** * Checks whether the given result has an assessment note that is not empty. * @param that the result of which the presence of an assessment note is being checked diff --git a/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.ts b/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.ts index 34aa3e0b0d5a..ad6ed37728d2 100644 --- a/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.ts +++ b/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.ts @@ -26,6 +26,7 @@ import { ExerciseHint } from 'app/entities/hestia/exercise-hint.model'; import { ExerciseHintService } from 'app/exercises/shared/exercise-hint/shared/exercise-hint.service'; import { HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; +import { isManualResult as isManualResultFunction } from 'app/exercises/shared/result/result.utils'; @Component({ selector: 'jhi-code-editor-student', @@ -148,7 +149,7 @@ export class CodeEditorStudentContainerComponent implements OnInit, OnDestroy { let hasTutorFeedback = false; if (this.latestResult) { // latest result is the first element of results, see loadParticipationWithLatestResult - isManualResult = Result.isManualResult(this.latestResult); + isManualResult = isManualResultFunction(this.latestResult); if (isManualResult) { hasTutorFeedback = this.latestResult.feedbacks!.some((feedback) => feedback.type === FeedbackType.MANUAL); } diff --git a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-trigger-build-button.component.ts b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-trigger-build-button.component.ts index 52089b28c991..1431ad0bfe38 100644 --- a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-trigger-build-button.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-trigger-build-button.component.ts @@ -13,6 +13,7 @@ import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { AlertService } from 'app/core/util/alert.service'; import { hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; +import { isManualResult } from 'app/exercises/shared/result/result.utils'; /** * Component for triggering a build for the CURRENT submission of the student (does not create a new commit!). @@ -60,7 +61,7 @@ export abstract class ProgrammingExerciseTriggerBuildButtonComponent implements if (hasDueDatePassed(this.exercise)) { // If the last result was manual, the instructor might not want to override it with a new automatic result. const newestResult = !!this.participation.results && head(orderBy(this.participation.results, ['id'], ['desc'])); - this.lastResultIsManual = !!newestResult && Result.isManualResult(newestResult); + this.lastResultIsManual = !!newestResult && isManualResult(newestResult); } // We can trigger the build only if the participation is active (has build plan), if the build plan was archived (new build plan will be created) // or the due date is over. @@ -126,7 +127,7 @@ export abstract class ProgrammingExerciseTriggerBuildButtonComponent implements .pipe( filter((result) => !!result), tap((result: Result) => { - this.lastResultIsManual = !!result && Result.isManualResult(result); + this.lastResultIsManual = !!result && isManualResult(result); }), ) .subscribe(); diff --git a/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts b/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts index 75f9243432c3..259942878d42 100644 --- a/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts +++ b/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnChanges } from '@angular/core'; import { Submission, getLatestSubmissionResult } from 'app/entities/submission.model'; import { Result } from 'app/entities/result.model'; +import { isManualResult } from 'app/exercises/shared/result/result.utils'; @Component({ selector: 'jhi-assessment-progress-label', @@ -14,7 +15,7 @@ export class AssessmentProgressLabelComponent implements OnChanges { ngOnChanges() { this.numberAssessedSubmissions = this.submissions.filter((submission) => { const result = getLatestSubmissionResult(submission); - return result?.rated && Result.isManualResult(result) && result?.completionDate; + return result?.rated && isManualResult(result) && result?.completionDate; }).length; } } diff --git a/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts b/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts index a5bffcbe5575..715a74ab8414 100644 --- a/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts +++ b/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts @@ -48,6 +48,7 @@ import { faCheckCircle, faExclamationTriangle, faFolderOpen, faListAlt, faQuesti import { GraphColors } from 'app/entities/statistics.model'; import { PROFILE_LOCALVC } from 'app/app.constants'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { isManualResult } from 'app/exercises/shared/result/result.utils'; export interface ExampleSubmissionQueryParams { readOnly?: boolean; @@ -640,7 +641,7 @@ export class ExerciseAssessmentDashboardComponent implements OnInit { */ calculateSubmissionStatusIsDraft(submission: Submission, correctionRound = 0): boolean { const tmpResult = submission.results?.[correctionRound]; - return !(tmpResult?.completionDate && Result.isManualResult(tmpResult)); + return !(tmpResult?.completionDate && isManualResult(tmpResult)); } /** diff --git a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts index c2642528bc00..f79a1114d043 100644 --- a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts @@ -27,6 +27,7 @@ import dayjs from 'dayjs/esm'; import { ExerciseCacheService } from 'app/exercises/shared/exercise/exercise-cache.service'; import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; import { PROFILE_LOCALVC } from 'app/app.constants'; +import { isManualResult } from 'app/exercises/shared/result/result.utils'; /** * Filter properties for a result @@ -229,7 +230,7 @@ export class ExerciseScoresComponent implements OnInit, OnDestroy { case FilterProp.BUILD_FAILED: return !!(participation.submissions?.[0] && (participation.submissions?.[0] as ProgrammingSubmission).buildFailed); case FilterProp.MANUAL: - return !!latestResult && Result.isManualResult(latestResult); + return !!latestResult && isManualResult(latestResult); case FilterProp.AUTOMATIC: return latestResult?.assessmentType === AssessmentType.AUTOMATIC; case FilterProp.LOCKED: 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 f126b6d7679f..23ecac9676b2 100644 --- a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html +++ b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html @@ -119,15 +119,15 @@

{{ 'artemisApp.result.preliminary' | artemisTranslate | uppercase }}
- @if (result?.assessmentType !== AssessmentType.AUTOMATIC_ATHENA) { + @if (result?.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { +

+ } @else { @if (exercise?.assessmentType !== AssessmentType.AUTOMATIC) {

} @if (exercise?.assessmentType === AssessmentType.AUTOMATIC) {

} - } @else { -

}

} diff --git a/src/main/webapp/app/exercises/shared/participation/participation.utils.ts b/src/main/webapp/app/exercises/shared/participation/participation.utils.ts index fac8c117f2f5..d931734c6407 100644 --- a/src/main/webapp/app/exercises/shared/participation/participation.utils.ts +++ b/src/main/webapp/app/exercises/shared/participation/participation.utils.ts @@ -116,9 +116,10 @@ export function getLatestResultOfStudentParticipation( if (participation.results) { participation.results = _orderBy(participation.results, 'completionDate', 'desc'); } + // The latest result is the first rated result in the sorted array (=newest) or any result if the option is active to show ungraded results. const latestResult = participation.results?.find( - (result) => showUngradedResults || result.rated === true || (showAthenaPreliminaryFeedback && !!isAIResultAndIsBeingProcessed(result)), + (result) => showUngradedResults || result.rated === true || (showAthenaPreliminaryFeedback && isAIResultAndIsBeingProcessed(result)), ); // Make sure that the participation result is connected to the newest result. return latestResult ? { ...latestResult, participation: participation } : undefined; diff --git a/src/main/webapp/app/exercises/shared/result/result.component.ts b/src/main/webapp/app/exercises/shared/result/result.component.ts index 3415021e11c7..f9edf80994d9 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/result.component.ts @@ -1,6 +1,13 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, Optional, SimpleChanges } from '@angular/core'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; -import { MissingResultInformation, ResultTemplateStatus, evaluateTemplateStatus, getResultIconClass, getTextColorClass } from 'app/exercises/shared/result/result.utils'; +import { + MissingResultInformation, + ResultTemplateStatus, + evaluateTemplateStatus, + getResultIconClass, + getTextColorClass, + isAthenaAIResult, +} from 'app/exercises/shared/result/result.utils'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; @@ -190,7 +197,7 @@ export class ResultComponent implements OnInit, OnChanges, OnDestroy { this.resultString = this.resultService.getResultString(this.result, this.exercise, this.short); } else if ( this.result && - ((this.result.score !== undefined && (this.result.rated || this.result.rated == undefined || this.showUngradedResults)) || Result.isAthenaAIResult(this.result)) + ((this.result.score !== undefined && (this.result.rated || this.result.rated == undefined || this.showUngradedResults)) || isAthenaAIResult(this.result)) ) { this.textColorClass = getTextColorClass(this.result, this.templateStatus); this.resultIconClass = getResultIconClass(this.result, this.templateStatus); @@ -230,7 +237,7 @@ export class ResultComponent implements OnInit, OnChanges, OnDestroy { return 'artemisApp.result.resultString.automaticAIFeedbackTimedOutTooltip'; } else if (this.templateStatus === ResultTemplateStatus.IS_GENERATING_FEEDBACK) { return 'artemisApp.result.resultString.automaticAIFeedbackInProgressTooltip'; - } else if (this.templateStatus === ResultTemplateStatus.HAS_RESULT && Result.isAthenaAIResult(this.result)) { + } else if (this.templateStatus === ResultTemplateStatus.HAS_RESULT && isAthenaAIResult(this.result)) { return 'artemisApp.result.resultString.automaticAIFeedbackSuccessfulTooltip'; } } diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index 2e3c35837532..ea4c9eaaca4c 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -24,6 +24,7 @@ import { isAIResultAndIsBeingProcessed, isAIResultAndProcessed, isAIResultAndTimedOut, + isAthenaAIResult, isStudentParticipation, } from 'app/exercises/shared/result/result.utils'; import { CsvDownloadService } from 'app/shared/util/CsvDownloadService'; @@ -94,7 +95,7 @@ export class ResultService implements IResultService { const relativeScore = roundValueSpecifiedByCourseSettings(result.score!, getCourseFromExercise(exercise)); const points = roundValueSpecifiedByCourseSettings((result.score! * exercise.maxPoints!) / 100, getCourseFromExercise(exercise)); if (exercise.type !== ExerciseType.PROGRAMMING) { - if (Result.isAthenaAIResult(result)) { + if (isAthenaAIResult(result)) { return this.getResultStringNonProgrammingExerciseWithAIFeedback(result, relativeScore, points, short); } return this.getResultStringNonProgrammingExercise(relativeScore, points, short); @@ -112,7 +113,7 @@ export class ResultService implements IResultService { */ private getResultStringNonProgrammingExerciseWithAIFeedback(result: Result, relativeScore: number, points: number, short: boolean | undefined): string { let aiFeedbackMessage: string = ''; - if (result && Result.isAthenaAIResult(result) && result.successful === undefined) { + if (result && isAthenaAIResult(result) && result.successful === undefined) { return this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackInProgress'); } aiFeedbackMessage = this.getResultStringNonProgrammingExercise(relativeScore, points, short); @@ -187,7 +188,7 @@ export class ResultService implements IResultService { * @param short flag that indicates if the resultString should use the short format */ private getBaseResultStringProgrammingExercise(result: Result, relativeScore: number, points: number, buildAndTestMessage: string, short: boolean | undefined): string { - if (Result.isAthenaAIResult(result)) { + if (isAthenaAIResult(result)) { return buildAndTestMessage; } if (short) { diff --git a/src/main/webapp/app/exercises/shared/result/result.utils.ts b/src/main/webapp/app/exercises/shared/result/result.utils.ts index 5348beb48d55..cee9c6dbb07f 100644 --- a/src/main/webapp/app/exercises/shared/result/result.utils.ts +++ b/src/main/webapp/app/exercises/shared/result/result.utils.ts @@ -119,20 +119,29 @@ export const getUnreferencedFeedback = (feedbacks: Feedback[] | undefined): Feed return feedbacks ? feedbacks.filter((feedbackElement) => !feedbackElement.reference && feedbackElement.type === FeedbackType.MANUAL_UNREFERENCED) : undefined; }; -export function isAIResultAndFailed(result: Result | undefined) { - return result && Result.isAthenaAIResult(result) && result.successful === false; +export function isAIResultAndFailed(result: Result | undefined): boolean { + return (result && isAthenaAIResult(result) && result.successful === false) ?? false; } -export function isAIResultAndTimedOut(result: Result | undefined) { - return result && Result.isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isAfter(result.completionDate); +export function isAIResultAndTimedOut(result: Result | undefined): boolean { + return (result && isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isAfter(result.completionDate)) ?? false; } -export function isAIResultAndProcessed(result: Result | undefined) { - return result && Result.isAthenaAIResult(result) && result.successful === true; +export function isAIResultAndProcessed(result: Result | undefined): boolean { + return (result && isAthenaAIResult(result) && result.successful === true) ?? false; } -export function isAIResultAndIsBeingProcessed(result: Result | undefined) { - return result && Result.isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isSameOrBefore(result.completionDate); +export function isAIResultAndIsBeingProcessed(result: Result | undefined): boolean { + return (result && isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isSameOrBefore(result.completionDate)) ?? false; +} + +/** + * Checks whether the result is generated by Athena AI. + * + * @return true if the result is an automatic Athena AI result + */ +export function isAthenaAIResult(result: Result): boolean { + return result.assessmentType === AssessmentType.AUTOMATIC_ATHENA; } export const evaluateTemplateStatus = ( diff --git a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts index bee34377e6f8..640b02f38bff 100644 --- a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts @@ -13,7 +13,7 @@ import { StudentParticipation } from 'app/entities/participation/student-partici import { Result } from 'app/entities/result.model'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { getLatestResultOfStudentParticipation, hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; -import { MissingResultInformation, isAIResultAndIsBeingProcessed } from 'app/exercises/shared/result/result.utils'; +import { MissingResultInformation, isAIResultAndIsBeingProcessed, isAthenaAIResult } from 'app/exercises/shared/result/result.utils'; import { convertDateFromServer } from 'app/utils/date.utils'; /** @@ -102,10 +102,10 @@ export class UpdatingResultComponent implements OnChanges, OnDestroy { filter((result) => !!result), // Ignore ungraded results if ungraded results are supposed to be ignored. // If the result is a preliminary feedback(being generated), show it - filter((result: Result) => this.showUngradedResults || result.rated === true || Result.isAthenaAIResult(result)), + filter((result: Result) => this.showUngradedResults || result.rated === true || isAthenaAIResult(result)), map((result) => ({ ...result, completionDate: convertDateFromServer(result.completionDate), participation: this.participation })), tap((result) => { - if ((Result.isAthenaAIResult(result) && isAIResultAndIsBeingProcessed(result)) || result.rated) { + if ((isAthenaAIResult(result) && isAIResultAndIsBeingProcessed(result)) || result.rated) { this.result = result; } else if (result.rated === false && this.showUngradedResults) { this.result = result; diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts index 5fd64ee3d6b6..a32ffcb632c5 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts @@ -340,7 +340,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges * 3. There is no already pending feedback request. * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. */ - // this method will be removed once text and modeling support new component + // TODO remove this method once support of the button component is implemented for text and modeling exercises assureConditionsSatisfied(): boolean { this.updateParticipations(); if (this.exercise.type === ExerciseType.PROGRAMMING) { @@ -378,7 +378,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges } } - if (this.hasAthenaResultForlatestSubmission()) { + if (this.hasAthenaResultForLatestSubmission()) { const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); this.alertService.warning(submitFirstWarning); return false; @@ -386,9 +386,9 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges return true; } - hasAthenaResultForlatestSubmission(): boolean { + hasAthenaResultForLatestSubmission(): boolean { if (this.gradedParticipation?.submissions && this.gradedParticipation?.results) { - // submissions.results is always undefined so this is neccessary + // submissions.results is always undefined so this is necessary return ( this.gradedParticipation.submissions.last()?.id === this.gradedParticipation?.results.filter((result) => result.assessmentType == AssessmentType.AUTOMATIC_ATHENA).first()?.submission?.id diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index 4457996c9a1d..b71d5bc00c5c 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -96,12 +96,12 @@ export class RequestFeedbackButtonComponent implements OnInit { * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. */ assureConditionsSatisfied(): boolean { - if (this.exercise().type !== ExerciseType.PROGRAMMING && this.hasAthenaResultForLatestSubmission()) { - const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); - this.alertService.warning(submitFirstWarning); + if (this.exercise().type === ExerciseType.PROGRAMMING || !this.hasAthenaResultForLatestSubmission()) { return false; } - return true; + const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); + this.alertService.warning(submitFirstWarning); + return false; } hasAthenaResultForLatestSubmission(): boolean { From 82ac204306849acee78adf7db99d247029b11ffe Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Thu, 3 Oct 2024 04:27:59 +0200 Subject: [PATCH 25/28] fix test --- .../javascript/spec/component/shared/result.component.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/javascript/spec/component/shared/result.component.spec.ts b/src/test/javascript/spec/component/shared/result.component.spec.ts index d72fdc5f3bd2..e013c1e03e58 100644 --- a/src/test/javascript/spec/component/shared/result.component.spec.ts +++ b/src/test/javascript/spec/component/shared/result.component.spec.ts @@ -370,7 +370,6 @@ describe('ResultComponent', () => { it('should use special handling if result is an automatic AI result', () => { comp.result = { ...mockResult, score: 90, assessmentType: AssessmentType.AUTOMATIC_ATHENA }; - jest.spyOn(Result, 'isAthenaAIResult').mockReturnValue(true); comp.evaluate(); From 9a4fec59127c5264f24259376bab1a5774f3517b Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Thu, 3 Oct 2024 04:55:19 +0200 Subject: [PATCH 26/28] improve client style --- .../assessment-progress-label.component.ts | 1 - .../dashboards/tutor/exercise-assessment-dashboard.component.ts | 1 - .../shared/exercise-scores/exercise-scores.component.ts | 1 - .../request-feedback-button.component.ts | 2 +- 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts b/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts index 259942878d42..254a3b2a5f82 100644 --- a/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts +++ b/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts @@ -1,6 +1,5 @@ import { Component, Input, OnChanges } from '@angular/core'; import { Submission, getLatestSubmissionResult } from 'app/entities/submission.model'; -import { Result } from 'app/entities/result.model'; import { isManualResult } from 'app/exercises/shared/result/result.utils'; @Component({ diff --git a/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts b/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts index 715a74ab8414..63a23747c1f9 100644 --- a/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts +++ b/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts @@ -42,7 +42,6 @@ import { ArtemisNavigationUtilService, getLinkToSubmissionAssessment } from 'app import { AssessmentType } from 'app/entities/assessment-type.model'; import { LegendPosition } from '@swimlane/ngx-charts'; import { AssessmentDashboardInformationEntry } from 'app/course/dashboards/assessment-dashboard/assessment-dashboard-information.component'; -import { Result } from 'app/entities/result.model'; import dayjs from 'dayjs/esm'; import { faCheckCircle, faExclamationTriangle, faFolderOpen, faListAlt, faQuestionCircle, faSort, faSpinner } from '@fortawesome/free-solid-svg-icons'; import { GraphColors } from 'app/entities/statistics.model'; diff --git a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts index f79a1114d043..dbc3b28d2e83 100644 --- a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts @@ -13,7 +13,6 @@ import { areManualResultsAllowed } from 'app/exercises/shared/exercise/exercise. import { ResultService } from 'app/exercises/shared/result/result.service'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; -import { Result } from 'app/entities/result.model'; import { ProgrammingSubmission } from 'app/entities/programming/programming-submission.model'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AssessmentType } from 'app/entities/assessment-type.model'; diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts index b71d5bc00c5c..b9aecaec56d5 100644 --- a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -97,7 +97,7 @@ export class RequestFeedbackButtonComponent implements OnInit { */ assureConditionsSatisfied(): boolean { if (this.exercise().type === ExerciseType.PROGRAMMING || !this.hasAthenaResultForLatestSubmission()) { - return false; + return true; } const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); this.alertService.warning(submitFirstWarning); From 2587bf5d11f004f59a7c1a15750f058866a2924c Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Fri, 4 Oct 2024 13:31:03 +0200 Subject: [PATCH 27/28] CR of Julian --- .../cit/aet/artemis/assessment/domain/Result.java | 2 +- .../cit/aet/artemis/exercise/domain/Exercise.java | 2 +- .../aet/artemis/exercise/domain/Submission.java | 9 ++++----- .../programming/domain/ProgrammingExercise.java | 3 +-- .../exercise-details-student-actions.component.ts | 2 +- .../ParticipationIntegrationTest.java | 14 +++++++------- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java b/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java index cc14d7a35e34..77c01c6fae19 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java @@ -629,7 +629,7 @@ public boolean isAutomatic() { * @return true if the result is an automatic AI Athena result */ @JsonIgnore - public boolean isAthenaAutomatic() { + public boolean isAthenaBased() { return AssessmentType.AUTOMATIC_ATHENA == assessmentType; } diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java index 3c3f1f26a239..b25eb7ab154d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java @@ -563,7 +563,7 @@ public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Particip boolean noProgrammingAndAssessmentOver = !isProgrammingExercise && isAssessmentOver; // For programming exercises we check that the assessment due date has passed (if set) for manual results otherwise we always show the automatic result boolean programmingAfterAssessmentOrAutomaticOrAthena = isProgrammingExercise - && ((result.isManual() && isAssessmentOver) || result.isAutomatic() || result.isAthenaAutomatic()); + && ((result.isManual() && isAssessmentOver) || result.isAutomatic() || result.isAthenaBased()); if (ratedOrPractice && (noProgrammingAndAssessmentOver || programmingAfterAssessmentOrAutomaticOrAthena)) { // take the first found result that fulfills the above requirements // or diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java index 326507d47dd4..304c206938b8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java @@ -162,7 +162,7 @@ public Result getResultForCorrectionRound(int correctionRound) { */ @NotNull private List filterNonAutomaticResults() { - return results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaAutomatic())).toList(); + return results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaBased())).toList(); } /** @@ -188,8 +188,7 @@ public boolean hasResultForCorrectionRound(int correctionRound) { */ @JsonIgnore public void removeAutomaticResults() { - this.results = this.results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaAutomatic())) - .collect(Collectors.toCollection(ArrayList::new)); + this.results = this.results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaBased())).collect(Collectors.toCollection(ArrayList::new)); } /** @@ -214,7 +213,7 @@ public List getResults() { @JsonIgnore public List getManualResults() { - return results.stream().filter(result -> result != null && !result.isAutomatic() && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new)); + return results.stream().filter(result -> result != null && !result.isAutomatic() && !result.isAthenaBased()).collect(Collectors.toCollection(ArrayList::new)); } /** @@ -224,7 +223,7 @@ public List getManualResults() { */ @JsonIgnore public List getNonAthenaResults() { - return results.stream().filter(result -> result != null && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new)); + return results.stream().filter(result -> result != null && !result.isAthenaBased()).collect(Collectors.toCollection(ArrayList::new)); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java index df7911670a22..c2a4666c7c1b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java @@ -712,8 +712,7 @@ private boolean checkForRatedAndAssessedResult(Result result) { * @return true if the result is manual and the assessment is over, or it is an automatic result, false otherwise */ private boolean checkForAssessedResult(Result result) { - return result.getCompletionDate() != null - && ((result.isManual() && ExerciseDateService.isAfterAssessmentDueDate(this)) || result.isAutomatic() || result.isAthenaAutomatic()); + return result.getCompletionDate() != null && ((result.isManual() && ExerciseDateService.isAfterAssessmentDueDate(this)) || result.isAutomatic() || result.isAthenaBased()); } @Override diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts index a32ffcb632c5..d8be7b4e3d2d 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts @@ -255,7 +255,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges }); } - // this method will be removed once text and modeling support new component + // TODO remove this method once support of the button component is implemented for text and modeling exercises requestFeedback() { if (!this.assureConditionsSatisfied()) return; if (this.exercise.type === ExerciseType.PROGRAMMING) { diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java index 66be8104ac5b..f3c5b3e5d1c9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java @@ -573,7 +573,7 @@ void requestProgrammingFeedbackIfARequestAlreadySent_withAthenaSuccess() throws assertThat(invokedResult).isNotNull(); assertThat(invokedResult.getId()).isNotNull(); assertThat(invokedResult.isSuccessful()).isTrue(); - assertThat(invokedResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedResult.isAthenaBased()).isTrue(); assertThat(invokedResult.getFeedbacks()).hasSize(1); localRepo.resetLocalRepo(); @@ -617,7 +617,7 @@ void requestProgrammingFeedbackSuccess_withAthenaSuccess() throws Exception { assertThat(invokedResult).isNotNull(); assertThat(invokedResult.getId()).isNotNull(); assertThat(invokedResult.isSuccessful()).isTrue(); - assertThat(invokedResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedResult.isAthenaBased()).isTrue(); assertThat(invokedResult.getFeedbacks()).hasSize(1); localRepo.resetLocalRepo(); @@ -654,7 +654,7 @@ void requestTextFeedbackSuccess_withAthenaSuccess() throws Exception { Result invokedTextResult = resultCaptor.getAllValues().get(1); assertThat(invokedTextResult).isNotNull(); assertThat(invokedTextResult.getId()).isNotNull(); - assertThat(invokedTextResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedTextResult.isAthenaBased()).isTrue(); assertThat(invokedTextResult.getFeedbacks()).hasSize(1); } @@ -689,7 +689,7 @@ void requestModelingFeedbackSuccess_withAthenaSuccess() throws Exception { Result invokedModelingResult = resultCaptor.getAllValues().get(1); assertThat(invokedModelingResult).isNotNull(); assertThat(invokedModelingResult.getId()).isNotNull(); - assertThat(invokedModelingResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedModelingResult.isAthenaBased()).isTrue(); assertThat(invokedModelingResult.getFeedbacks()).hasSize(1); } @@ -730,7 +730,7 @@ void requestProgrammingFeedbackSuccess_withAthenaFailure() throws Exception { assertThat(invokedResult).isNotNull(); assertThat(invokedResult.getId()).isNotNull(); assertThat(invokedResult.isSuccessful()).isFalse(); - assertThat(invokedResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedResult.isAthenaBased()).isTrue(); assertThat(invokedResult.getFeedbacks()).hasSize(0); localRepo.resetLocalRepo(); @@ -766,7 +766,7 @@ void requestTextFeedbackSuccess_withAthenaFailure() throws Exception { Result invokedTextResult = resultCaptor.getAllValues().getFirst(); assertThat(invokedTextResult).isNotNull(); - assertThat(invokedTextResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedTextResult.isAthenaBased()).isTrue(); assertThat(invokedTextResult.getFeedbacks()).hasSize(0); } @@ -800,7 +800,7 @@ void requestModelingFeedbackSuccess_withAthenaFailure() throws Exception { Result invokedModelingResult = resultCaptor.getAllValues().getFirst(); assertThat(invokedModelingResult).isNotNull(); - assertThat(invokedModelingResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedModelingResult.isAthenaBased()).isTrue(); assertThat(invokedModelingResult.getFeedbacks()).hasSize(0); } From 2582aab218a3d69f12f29ad113a6a5791fc68ce7 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka Date: Sat, 5 Oct 2024 15:25:41 +0200 Subject: [PATCH 28/28] set athenaEnabled for modeling exercises --- .../exercise-details-student-actions.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts index d8be7b4e3d2d..2991bac3355f 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts @@ -136,6 +136,9 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges }); } else if (this.exercise.type === ExerciseType.MODELING) { this.editorLabel = 'openModelingEditor'; + this.profileService.getProfileInfo().subscribe((profileInfo) => { + this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); + }); } else if (this.exercise.type === ExerciseType.TEXT) { this.editorLabel = 'openTextEditor'; this.profileService.getProfileInfo().subscribe((profileInfo) => {