Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

General:Make Request Feedback a standalone component #9323

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -382,7 +381,7 @@ private ResponseEntity<StudentParticipation> 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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consider using AccessForbiddenException instead of BadRequestAlertException

Requesting feedback after the due date is a forbidden action. Using AccessForbiddenException, which results in a 403 Forbidden status code, would more accurately represent the access restriction compared to BadRequestAlertException (400 Bad Request).

}
if (exercise instanceof ProgrammingExercise) {
((ProgrammingExercise) exercise).validateSettingsForFeedbackRequest();
Expand All @@ -393,7 +392,7 @@ private ResponseEntity<StudentParticipation> 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));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use EntityNotFoundException for missing participation

When a participation is not found, it's appropriate to throw an EntityNotFoundException, which corresponds to a 404 Not Found status code. This provides a clearer indication of the error compared to BadRequestAlertException (400 Bad Request).


checkAccessPermissionOwner(participation, user);
participation = studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId());
Expand All @@ -406,15 +405,15 @@ private ResponseEntity<StudentParticipation> 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);
undernagruzez marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Check if feedback has already been requested
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix typo in error code "noSubmissionÊxists"

The error code in the BadRequestAlertException should likely be "noSubmissionExists" without the special character 'Ê'.

Apply this diff to correct the typo:

-throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionÊxists");
+throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionExists");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionÊxists");
throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionExists");

}
var submission = submissionOptional.get();

Expand Down Expand Up @@ -225,15 +225,10 @@ private void checkRateLimitOrThrow(ProgrammingExerciseStudentParticipation parti

List<Result> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
@if (!!participation()?.exercise) {
<jhi-request-feedback-button class="me-3" [exercise]="participation()?.exercise!"></jhi-request-feedback-button>
}
@if (commitState === CommitState.CONFLICT) {
<div>
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, input } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { catchError, switchMap, tap } from 'rxjs/operators';
Expand All @@ -14,6 +14,7 @@ import { CodeEditorConfirmRefreshModalComponent } from './code-editor-confirm-re
import { AUTOSAVE_CHECK_INTERVAL, AUTOSAVE_EXERCISE_INTERVAL } from 'app/shared/constants/exercise-exam-constants';
import { faCircleNotch, faSync, faTimes } from '@fortawesome/free-solid-svg-icons';
import { faPlayCircle } from '@fortawesome/free-regular-svg-icons';
import { Participation } from 'app/entities/participation/participation.model';

@Component({
selector: 'jhi-code-editor-actions',
Expand All @@ -34,6 +35,7 @@ export class CodeEditorActionsComponent implements OnInit, OnDestroy, OnChanges
@Input() get commitState() {
return this.commitStateValue;
}
participation = input<Participation>();

undernagruzez marked this conversation as resolved.
Show resolved Hide resolved
@Output() commitStateChange = new EventEmitter<CommitState>();
@Output() editorStateChange = new EventEmitter<EditorState>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { CodeEditorFileBrowserBadgeComponent } from 'app/exercises/programming/s
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 { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module';
import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component';

@NgModule({
imports: [
Expand All @@ -35,6 +36,7 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo
ArtemisProgrammingManualAssessmentModule,
MonacoEditorModule,
ArtemisSharedComponentModule,
RequestFeedbackButtonComponent,
],
declarations: [
CodeEditorGridComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ <h4 class="editor-title"><ng-content select="[editorTitle]" /></h4>
(onRefreshFiles)="onRefreshFiles()"
(commitStateChange)="onCommitStateChange.emit($event)"
(onError)="onError($event)"
[participation]="participation"
undernagruzez marked this conversation as resolved.
Show resolved Hide resolved
/>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,30 +135,8 @@
<span class="d-none d-md-inline" jhiTranslate="artemisApp.exerciseActions.openOnlineIDE"></span>
</a>
}
@if (exercise.allowFeedbackRequests) {
@if (athenaEnabled) {
<a
class="btn btn-primary"
(click)="requestFeedback()"
[class.btn-sm]="smallButtons"
[id]="'request-feedback-' + exercise.id"
[ngbTooltip]="'artemisApp.exerciseActions.requestAutomaticFeedbackTooltip' | artemisTranslate"
>
<fa-icon [icon]="faPenSquare" [fixedWidth]="true" />
<span class="d-none d-md-inline" jhiTranslate="artemisApp.exerciseActions.requestAutomaticFeedback">Send automatic feedback request</span>
</a>
} @else {
<a
class="btn btn-primary"
(click)="requestFeedback()"
[class.btn-sm]="smallButtons"
[id]="'request-feedback-' + exercise.id"
[ngbTooltip]="'artemisApp.exerciseActions.requestManualFeedbackTooltip' | artemisTranslate"
>
<fa-icon [icon]="faPenSquare" [fixedWidth]="true" />
<span class="d-none d-md-inline" jhiTranslate="artemisApp.exerciseActions.requestManualFeedback">Send manual feedback request</span>
</a>
}
@if (exercise.allowFeedbackRequests && gradedParticipation && exercise.type === ExerciseType.PROGRAMMING) {
<jhi-request-feedback-button [exercise]="exercise" [smallButtons]="smallButtons"></jhi-request-feedback-button>
}
undernagruzez marked this conversation as resolved.
Show resolved Hide resolved
</ng-container>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges
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) {
this.theiaEnabled = true;
Expand All @@ -135,9 +136,6 @@ 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) => {
Expand Down Expand Up @@ -257,6 +255,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges
});
}

// this method will be removed once text and modeling support new component
requestFeedback() {
if (!this.assureConditionsSatisfied()) return;
if (this.exercise.type === ExerciseType.PROGRAMMING) {
Expand Down Expand Up @@ -341,6 +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
assureConditionsSatisfied(): boolean {
this.updateParticipations();
if (this.exercise.type === ExerciseType.PROGRAMMING) {
Expand Down Expand Up @@ -388,27 +388,12 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges

hasAthenaResultForlatestSubmission(): boolean {
if (this.gradedParticipation?.submissions && this.gradedParticipation?.results) {
const sortedSubmissions = this.gradedParticipation.submissions.slice().sort((a, b) => {
const dateA = this.getDateValue(a.submissionDate) ?? -Infinity;
const dateB = this.getDateValue(b.submissionDate) ?? -Infinity;
return dateB - dateA;
});

return this.gradedParticipation.results.some((result) => result.submission?.id === sortedSubmissions[0]?.id);
// 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
);
Comment on lines +391 to +395
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential runtime errors due to non-standard first() and last() methods on arrays

JavaScript arrays do not have first() and last() methods by default, which could lead to runtime errors. Consider using array indexing to access the first and last elements.

Apply this diff to fix the issue:

-return (
-    this.gradedParticipation.submissions.last()?.id ===
-    this.gradedParticipation?.results.filter((result) => result.assessmentType == AssessmentType.AUTOMATIC_ATHENA).first()?.submission?.id
-);
+const latestSubmissionId = this.gradedParticipation.submissions?.[this.gradedParticipation.submissions.length - 1]?.id;
+const firstAthenaResultSubmissionId = this.gradedParticipation?.results.filter((result) => result.assessmentType === AssessmentType.AUTOMATIC_ATHENA)?.[0]?.submission?.id;
+return latestSubmissionId === firstAthenaResultSubmissionId;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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
);
// submissions.results is always undefined so this is necessary
const latestSubmissionId = this.gradedParticipation.submissions?.[this.gradedParticipation.submissions.length - 1]?.id;
const firstAthenaResultSubmissionId = this.gradedParticipation?.results.filter((result) => result.assessmentType === AssessmentType.AUTOMATIC_ATHENA)?.[0]?.submission?.id;
return latestSubmissionId === firstAthenaResultSubmissionId;

}
return false;
}

private getDateValue = (date: any): number => {
if (dayjs.isDayjs(date)) {
return date.valueOf();
}
if (date instanceof Date) {
return date.valueOf();
}
if (typeof date === 'string') {
return new Date(date).valueOf();
}
return -Infinity; // fallback for null, undefined, or invalid dates
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@if (!isExamExercise) {
@if (athenaEnabled) {
@if (exercise().type === ExerciseType.TEXT) {
<button
class="btn btn-primary"
(click)="requestFeedback()"
[class.btn-sm]="smallButtons()"
[disabled]="!participation?.submissions?.last()?.submitted || isGeneratingFeedback()"
[id]="'request-feedback-' + exercise().id"
[ngbTooltip]="'artemisApp.exerciseActions.requestAutomaticFeedbackTooltip' | artemisTranslate"
>
<fa-icon [icon]="faPenSquare" [fixedWidth]="true" />
<span class="d-none d-md-inline" jhiTranslate="artemisApp.exerciseActions.requestAutomaticFeedback"></span>
</button>
} @else {
<a
class="btn btn-primary"
(click)="requestFeedback()"
[class.btn-sm]="smallButtons()"
[id]="'request-feedback-' + exercise().id"
[ngbTooltip]="'artemisApp.exerciseActions.requestAutomaticFeedbackTooltip' | artemisTranslate"
>
<fa-icon [icon]="faPenSquare" [fixedWidth]="true" />
<span class="d-none d-md-inline" jhiTranslate="artemisApp.exerciseActions.requestAutomaticFeedback"></span>
</a>
undernagruzez marked this conversation as resolved.
Show resolved Hide resolved
}
} @else {
<a
class="btn btn-primary"
(click)="requestFeedback()"
[class.btn-sm]="smallButtons()"
[id]="'request-feedback-' + exercise().id"
[ngbTooltip]="'artemisApp.exerciseActions.requestManualFeedbackTooltip' | artemisTranslate"
>
<fa-icon [icon]="faPenSquare" [fixedWidth]="true" />
<span class="d-none d-md-inline" jhiTranslate="artemisApp.exerciseActions.requestManualFeedback"></span>
</a>
}
}
Loading
Loading