Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/chore/conversation-detail-tabs-m…
Browse files Browse the repository at this point in the history
…igration' into chore/conversation-detail-tabs-migration
  • Loading branch information
asliayk committed Dec 9, 2024
2 parents a75f02c + 5123fee commit 087351f
Show file tree
Hide file tree
Showing 21 changed files with 1,111 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ default List<Result> findLatestAutomaticResultsWithEagerFeedbacksTestCasesForExe

Optional<Result> findFirstByParticipationIdOrderByCompletionDateDesc(long participationId);

Optional<Result> findFirstByParticipationIdAndAssessmentTypeOrderByCompletionDateDesc(long participationId, AssessmentType assessmentType);

@EntityGraph(type = LOAD, attributePaths = { "feedbacks", "feedbacks.testCase" })
Optional<Result> findResultWithFeedbacksAndTestCasesById(long resultId);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package de.tum.cit.aet.artemis.athena.dto;

import java.util.List;

import jakarta.validation.constraints.NotNull;

import com.fasterxml.jackson.annotation.JsonInclude;
Expand All @@ -13,7 +11,7 @@
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record ModelingFeedbackDTO(long id, long exerciseId, long submissionId, String title, String description, double credits, Long structuredGradingInstructionId,
List<String> elementIds) implements FeedbackBaseDTO {
String reference) implements FeedbackBaseDTO {

/**
* Creates a ModelingFeedbackDTO from a Feedback object
Expand All @@ -30,6 +28,6 @@ public static ModelingFeedbackDTO of(long exerciseId, long submissionId, @NotNul
}

return new ModelingFeedbackDTO(feedback.getId(), exerciseId, submissionId, feedback.getText(), feedback.getDetailText(), feedback.getCredits(), gradingInstructionId,
List.of(feedback.getReference()));
feedback.getReference());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ public void stopContainer(String containerName) {
// Get the container ID.
String containerId = container.getId();

log.info("Stopping container with id {}", containerId);

// Create a file "stop_container.txt" in the root directory of the container to indicate that the test results have been extracted or that the container should be stopped
// for some other reason.
// The container's main process is waiting for this file to appear and then stops the main process, thus stopping and removing the container.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,18 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String

ZonedDateTime buildCompletedDate = ZonedDateTime.now();

msg = "~~~~~~~~~~~~~~~~~~~~ Moving test results to specified directory for build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~";
buildLogsMap.appendBuildLogEntry(buildJob.id(), msg);
log.debug(msg);

buildJobContainerService.moveResultsToSpecifiedDirectory(containerId, buildJob.buildConfig().resultPaths(), LOCALCI_WORKING_DIRECTORY + LOCALCI_RESULTS_DIRECTORY);

// Get an input stream of the test result files.

msg = "~~~~~~~~~~~~~~~~~~~~ Collecting test results from container " + containerId + " for build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~";
buildLogsMap.appendBuildLogEntry(buildJob.id(), msg);
log.info(msg);

TarArchiveInputStream testResultsTarInputStream;

try {
Expand Down Expand Up @@ -349,6 +357,10 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String
}
}

msg = "~~~~~~~~~~~~~~~~~~~~ Parsing test results for build job " + buildJob.id() + " ~~~~~~~~~~~~~~~~~~~~";
buildLogsMap.appendBuildLogEntry(buildJob.id(), msg);
log.info(msg);

BuildResult buildResult;
try {
buildResult = parseTestResults(testResultsTarInputStream, buildJob.buildConfig().branch(), assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate,
Expand All @@ -362,7 +374,7 @@ private BuildResult runScriptAndParseResults(BuildJobQueueItem buildJob, String
}

msg = "Building and testing submission for repository " + assignmentRepositoryUri.repositorySlug() + " and commit hash " + assignmentRepoCommitHash + " took "
+ TimeLogUtil.formatDurationFrom(timeNanoStart);
+ TimeLogUtil.formatDurationFrom(timeNanoStart) + " for build job " + buildJob.id();
buildLogsMap.appendBuildLogEntry(buildJob.id(), msg);
log.info(msg);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

Expand Down Expand Up @@ -176,6 +177,9 @@ public CompletableFuture<BuildResult> executeBuildJob(BuildJobQueueItem buildJob
}
else {
finishBuildJobExceptionally(buildJobItem.id(), containerName, e);
if (e instanceof TimeoutException) {
logTimedOutBuildJob(buildJobItem, buildJobTimeoutSeconds);
}
throw new CompletionException(e);
}
}
Expand All @@ -188,6 +192,18 @@ public CompletableFuture<BuildResult> executeBuildJob(BuildJobQueueItem buildJob
}));
}

private void logTimedOutBuildJob(BuildJobQueueItem buildJobItem, int buildJobTimeoutSeconds) {
String msg = "Timed out after " + buildJobTimeoutSeconds + " seconds. "
+ "This may be due to an infinite loop or inefficient code. Please review your code for potential issues. "
+ "If the problem persists, contact your instructor for assistance. (Build job ID: " + buildJobItem.id() + ")";
buildLogsMap.appendBuildLogEntry(buildJobItem.id(), msg);
log.warn(msg);

msg = "Executing build job with id " + buildJobItem.id() + " timed out after " + buildJobTimeoutSeconds + " seconds."
+ "This may be due to strict timeout settings. Consider increasing the exercise timeout and applying stricter timeout constraints within the test cases using @StrictTimeout.";
buildLogsMap.appendBuildLogEntry(buildJobItem.id(), msg);
}

Set<String> getRunningBuildJobIds() {
return Set.copyOf(runningFutures.keySet());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,6 @@ public ModelingExerciseFeedbackService(Optional<AthenaFeedbackSuggestionsService
this.participationService = participationService;
}

private void checkRateLimitOrThrow(StudentParticipation participation) {

List<Result> athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList();

if (athenaResults.size() >= 10) {
throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met");
}
}

/**
* Handles the request for generating feedback for a modeling exercise.
* Unlike programming exercises a tutor is not notified if Athena is not available.
Expand All @@ -79,6 +70,7 @@ private void checkRateLimitOrThrow(StudentParticipation participation) {
public StudentParticipation handleNonGradedFeedbackRequest(StudentParticipation participation, ModelingExercise modelingExercise) {
if (this.athenaFeedbackSuggestionsService.isPresent()) {
this.checkRateLimitOrThrow(participation);
this.checkLatestSubmissionHasAthenaResultOrThrow(participation);
CompletableFuture.runAsync(() -> this.generateAutomaticNonGradedFeedback(participation, modelingExercise));
}
return participation;
Expand Down Expand Up @@ -125,6 +117,10 @@ public void generateAutomaticNonGradedFeedback(StudentParticipation participatio
}
catch (Exception e) {
log.error("Could not generate feedback for exercise ID: {} and participation ID: {}", modelingExercise.getId(), participation.getId(), e);
automaticResult.setSuccessful(false);
automaticResult.setCompletionDate(null);
participation.addResult(automaticResult);
this.resultWebsocketService.broadcastNewResult(participation, automaticResult);
throw new InternalServerErrorException("Something went wrong... AI Feedback could not be generated");
}
}
Expand Down Expand Up @@ -173,6 +169,7 @@ private Feedback convertToFeedback(ModelingFeedbackDTO feedbackItem) {
feedback.setHasLongFeedbackText(false);
feedback.setType(FeedbackType.AUTOMATIC);
feedback.setCredits(feedbackItem.credits());
feedback.setReference(feedbackItem.reference());
return feedback;
}

Expand All @@ -193,4 +190,45 @@ private double calculateTotalFeedbackScore(List<Feedback> feedbacks, ModelingExe

return (totalCredits / maxPoints) * 100;
}

/**
* Checks if the number of Athena results for the given participation exceeds
* the allowed threshold and throws an exception if the limit is reached.
*
* @param participation the student participation to check
* @throws BadRequestAlertException if the maximum number of Athena feedback requests is exceeded
*/
private void checkRateLimitOrThrow(StudentParticipation participation) {
List<Result> athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList();

if (athenaResults.size() >= 10) {
throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true);
}
}

/**
* Ensures that the latest submission associated with the participation does not already
* have an Athena-generated result. Throws an exception if Athena result already exists.
*
* @param participation the student participation to validate
* @throws BadRequestAlertException if no legal submissions exist or if an Athena result is already present
*/
private void checkLatestSubmissionHasAthenaResultOrThrow(StudentParticipation participation) {
Optional<Submission> submissionOptional = participationService.findExerciseParticipationWithLatestSubmissionAndResultElseThrow(participation.getId())
.findLatestSubmission();

if (submissionOptional.isEmpty()) {
throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission");
}

Submission submission = submissionOptional.get();

Result latestResult = submission.getLatestResult();

if (latestResult != null && latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA) {
log.debug("Submission ID: {} already has an Athena result. Skipping feedback generation.", submission.getId());
throw new BadRequestAlertException("Submission already has an Athena result", "submission", "submissionAlreadyHasAthenaResult", true);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -31,6 +32,8 @@
import de.tum.cit.aet.artemis.assessment.domain.GradingCriterion;
import de.tum.cit.aet.artemis.assessment.domain.Result;
import de.tum.cit.aet.artemis.assessment.repository.GradingCriterionRepository;
import de.tum.cit.aet.artemis.assessment.repository.ResultRepository;
import de.tum.cit.aet.artemis.assessment.service.ResultService;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
Expand Down Expand Up @@ -67,6 +70,8 @@ public class ModelingSubmissionResource extends AbstractSubmissionResource {

private static final String ENTITY_NAME = "modelingSubmission";

private final ResultRepository resultRepository;

@Value("${jhipster.clientApp.name}")
private String applicationName;

Expand All @@ -82,17 +87,21 @@ public class ModelingSubmissionResource extends AbstractSubmissionResource {

private final PlagiarismService plagiarismService;

private final ResultService resultService;

public ModelingSubmissionResource(SubmissionRepository submissionRepository, ModelingSubmissionService modelingSubmissionService,
ModelingExerciseRepository modelingExerciseRepository, AuthorizationCheckService authCheckService, UserRepository userRepository, ExerciseRepository exerciseRepository,
GradingCriterionRepository gradingCriterionRepository, ExamSubmissionService examSubmissionService, StudentParticipationRepository studentParticipationRepository,
ModelingSubmissionRepository modelingSubmissionRepository, PlagiarismService plagiarismService) {
ModelingSubmissionRepository modelingSubmissionRepository, PlagiarismService plagiarismService, ResultService resultService, ResultRepository resultRepository) {
super(submissionRepository, authCheckService, userRepository, exerciseRepository, modelingSubmissionService, studentParticipationRepository);
this.modelingSubmissionService = modelingSubmissionService;
this.modelingExerciseRepository = modelingExerciseRepository;
this.gradingCriterionRepository = gradingCriterionRepository;
this.examSubmissionService = examSubmissionService;
this.modelingSubmissionRepository = modelingSubmissionRepository;
this.plagiarismService = plagiarismService;
this.resultService = resultService;
this.resultRepository = resultRepository;
}

/**
Expand Down Expand Up @@ -367,4 +376,81 @@ public ResponseEntity<ModelingSubmission> getLatestSubmissionForModelingEditor(@

return ResponseEntity.ok(modelingSubmission);
}

/**
* GET /participations/{participationId}/submissions-with-results : get submissions with results for a particular student participation.
* When the assessment period is not over yet, only submissions with Athena results are returned.
* When the assessment period is over, both Athena and normal results are returned.
*
* @param participationId the id of the participation for which to get the submissions with results
* @return the ResponseEntity with status 200 (OK) and with body the list of submissions with results and feedbacks, or with status 404 (Not Found) if the participation could
* not be found
*/
@GetMapping("participations/{participationId}/submissions-with-results")
@EnforceAtLeastStudent
public ResponseEntity<List<Submission>> getSubmissionsWithResultsForParticipation(@PathVariable long participationId) {
log.debug("REST request to get submissions with results for participation: {}", participationId);

// Retrieve and check the participation
StudentParticipation participation = studentParticipationRepository.findByIdWithLegalSubmissionsResultsFeedbackElseThrow(participationId);
User user = userRepository.getUserWithGroupsAndAuthorities();

if (participation.getExercise() == null) {
return ResponseEntity.badRequest()
.headers(HeaderUtil.createFailureAlert(applicationName, true, "modelingExercise", "exerciseEmpty", "The exercise belonging to the participation is null."))
.body(null);
}

if (!(participation.getExercise() instanceof ModelingExercise modelingExercise)) {
return ResponseEntity.badRequest().headers(
HeaderUtil.createFailureAlert(applicationName, true, "modelingExercise", "wrongExerciseType", "The exercise of the participation is not a modeling exercise."))
.body(null);
}

// Students can only see their own models (to prevent cheating). TAs, instructors and admins can see all models.
boolean isAtLeastTutor = authCheckService.isAtLeastTeachingAssistantForExercise(modelingExercise, user);
if (!(authCheckService.isOwnerOfParticipation(participation) || isAtLeastTutor)) {
throw new AccessForbiddenException();
}

// Exam exercises cannot be seen by students between the endDate and the publishResultDate
if (!authCheckService.isAllowedToGetExamResult(modelingExercise, participation, user)) {
throw new AccessForbiddenException();
}

boolean isStudent = !isAtLeastTutor;

// Get the submissions associated with the participation
Set<Submission> submissions = participation.getSubmissions();

// Filter submissions to only include those with relevant results
List<Submission> submissionsWithResults = submissions.stream().filter(submission -> {

submission.setParticipation(participation);

// Filter results within each submission based on assessment type and period
List<Result> filteredResults = submission.getResults().stream().filter(result -> {
if (isStudent) {
if (ExerciseDateService.isAfterAssessmentDueDate(modelingExercise)) {
return true; // Include all results if the assessment period is over
}
else {
return result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA; // Only include Athena results if the assessment period is not over
}
}
else {
return true; // Tutors and above can see all results
}
}).peek(Result::filterSensitiveInformation).sorted(Comparator.comparing(Result::getCompletionDate).reversed()).toList();

// Set filtered results back into the submission if any results remain after filtering
if (!filteredResults.isEmpty()) {
submission.setResults(filteredResults);
return true; // Include submission as it has relevant results
}
return false;
}).toList();

return ResponseEntity.ok().body(submissionsWithResults);
}
}
Loading

0 comments on commit 087351f

Please sign in to comment.