Skip to content

Commit

Permalink
Update Artemis4J to expose necessary data for `programming-lecture-ar…
Browse files Browse the repository at this point in the history
…temis-score-stats` (#96)

* Update Artemis4J to expose necessary data for `programming-lecture-artemis-score-stats`

* rename `onlyOwn` to `filterAssessedByTutor` in `ProgrammingExercise#fetchSubmissions`

This is to be consistent with the param in `ProgrammingSubmissionDTO` and I like that name more.

* only fetch the feedbacks when necessary

* do not expose `FeedbackDTO` in `TestResult`

* fix tests

* add test

* make `ResultDTO.fetchDetailedFeedback` static and apply spotless
  • Loading branch information
Luro02 authored Aug 22, 2024
1 parent 9728fd3 commit 05e30fd
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,45 @@

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import com.fasterxml.jackson.annotation.JsonProperty;
import edu.kit.kastel.sdq.artemis4j.ArtemisNetworkException;
import edu.kit.kastel.sdq.artemis4j.grading.Exercise;

public record ProgrammingSubmissionDTO(@JsonProperty long id, @JsonProperty ParticipationDTO participation, @JsonProperty String commitHash,
@JsonProperty boolean buildFailed, @JsonProperty List<ResultDTO> results, @JsonProperty ZonedDateTime submissionDate) {

/**
* Fetch all programming submissions for an exercise.
*
* @param client the artemis client to use
* @param exerciseId the id of the exercise {@link Exercise#getId()}
* @param correctionRound the correction round, 0 for the first correction
* round or if there is only one correction round
* @param filterAssessedByTutor whether to only fetch submissions that have been
* assessed by the currently logged-in user/tutor
* @return a list of programming submissions
* @throws ArtemisNetworkException if the request fails
*/
public static List<ProgrammingSubmissionDTO> fetchAll(ArtemisClient client, long exerciseId, int correctionRound, boolean filterAssessedByTutor)
throws ArtemisNetworkException {
var submissions = ArtemisRequest.get().path(List.of("exercises", exerciseId, "programming-submissions")).param("assessedByTutor", filterAssessedByTutor)
.param("correction-round", correctionRound).executeAndDecode(client, ProgrammingSubmissionDTO[].class);

List<ProgrammingSubmissionDTO> result = new ArrayList<>();
for (var submission : submissions) {
result.add(submission.fetchLongFeedback(client));
}

return result;
return new ArrayList<>(Arrays
.asList(ArtemisRequest.get().path(List.of("exercises", exerciseId, "programming-submissions")).param("assessedByTutor", filterAssessedByTutor)
.param("correction-round", correctionRound).executeAndDecode(client, ProgrammingSubmissionDTO[].class)));
}

public static ProgrammingSubmissionDTO lock(ArtemisClient client, long submissionId, int correctionRound) throws ArtemisNetworkException {
return ArtemisRequest.get().path(List.of("programming-submissions", submissionId, "lock")).param("correction-round", correctionRound)
.executeAndDecode(client, ProgrammingSubmissionDTO.class).fetchLongFeedback(client);
.executeAndDecode(client, ProgrammingSubmissionDTO.class);
}

public static Optional<ProgrammingSubmissionDTO> lockNextSubmission(ArtemisClient client, long exerciseId, int correctionRound)
throws ArtemisNetworkException {
// Artemis returns an empty string if there is no new submission to lock
var submission = ArtemisRequest.get().path(List.of("exercises", exerciseId, "programming-submission-without-assessment")).param("lock", true)
return ArtemisRequest.get().path(List.of("exercises", exerciseId, "programming-submission-without-assessment")).param("lock", true)
.param("correction-round", correctionRound).executeAndDecodeMaybe(client, ProgrammingSubmissionDTO.class);

if (submission.isPresent()) {
submission = Optional.of(submission.get().fetchLongFeedback(client));
}

return submission;
}

public static void cancelAssessment(ArtemisClient client, long submissionId) throws ArtemisNetworkException {
Expand All @@ -51,31 +52,7 @@ public static void saveAssessment(ArtemisClient client, long participationId, bo
ArtemisRequest.put().path(List.of("participations", participationId, "manual-results")).param("submit", submit).body(result).execute(client);
}

private ProgrammingSubmissionDTO fetchLongFeedback(ArtemisClient client) throws ArtemisNetworkException {
List<ResultDTO> results = new ArrayList<>(this.results().size());
for (var result : this.results()) {
if (result.feedbacks() == null) {
continue;
}

List<FeedbackDTO> cleanedFeedbacks = new ArrayList<>(result.feedbacks().size());
for (var feedback : result.feedbacks()) {
if (feedback == null) {
continue;
}

String detailText = feedback.detailText();
if (feedback.hasLongFeedbackText()) {
detailText = FeedbackDTO.fetchLongFeedback(client, result.id(), feedback.id());
}
cleanedFeedbacks.add(new FeedbackDTO(detailText, feedback));
}

ResultDTO newResult = new ResultDTO(result.id(), result.completionDate(), result.successful(), result.score(), result.rated(), cleanedFeedbacks,
result.assessor(), result.assessmentType());
results.add(newResult);
}

return new ProgrammingSubmissionDTO(this.id(), this.participation(), this.commitHash(), this.buildFailed(), results, this.submissionDate());
public List<ResultDTO> nonAutomaticResults() {
return this.results().stream().filter(r -> r.assessmentType() != AssessmentType.AUTOMATIC).toList();
}
}
46 changes: 46 additions & 0 deletions src/main/java/edu/kit/kastel/sdq/artemis4j/client/ResultDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
package edu.kit.kastel.sdq.artemis4j.client;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonProperty;
import edu.kit.kastel.sdq.artemis4j.ArtemisNetworkException;
import edu.kit.kastel.sdq.artemis4j.grading.ProgrammingSubmission;

/**
* A result of a student's submission.
Expand All @@ -23,4 +27,46 @@ public record ResultDTO(@JsonProperty long id, @JsonProperty ZonedDateTime compl
public static ResultDTO forAssessmentSubmission(long submissionId, double score, List<FeedbackDTO> feedbacks, UserDTO assessor) {
return new ResultDTO(submissionId, null, true, score, true, feedbacks, assessor, AssessmentType.SEMI_AUTOMATIC);
}

private static List<FeedbackDTO> fetchFeedbacks(ArtemisClient client, long resultId, long participationId) throws ArtemisNetworkException {
return Arrays.asList(ArtemisRequest.get().path(List.of("participations", participationId, "results", resultId, "details")).executeAndDecode(client,
FeedbackDTO[].class));
}

/**
* This will fetch the feedbacks for this result and load the long feedbacks if
* necessary.
*
* @param client the client to use
* @param resultId the result id
* @param participationId the participation id, likely
* {@link ProgrammingSubmission#getParticipationId()}
* @param feedbacks the feedbacks to load the details for or null if they
* should be fetched first
* @return the feedbacks
* @throws ArtemisNetworkException if it fails to fetch the feedbacks
*/
public static List<FeedbackDTO> fetchDetailedFeedbacks(ArtemisClient client, long resultId, long participationId, List<FeedbackDTO> feedbacks)
throws ArtemisNetworkException {
// Sometimes the feedbacks are not loaded, to fetch the long feedbacks, we need
// to load the feedbacks first
if (feedbacks == null) {
feedbacks = ResultDTO.fetchFeedbacks(client, resultId, participationId);
}

List<FeedbackDTO> cleanedFeedbacks = new ArrayList<>(feedbacks.size());
for (var feedback : feedbacks) {
if (feedback == null) {
continue;
}

String detailText = feedback.detailText();
if (feedback.hasLongFeedbackText()) {
detailText = FeedbackDTO.fetchLongFeedback(client, resultId, feedback.id());
}
cleanedFeedbacks.add(new FeedbackDTO(detailText, feedback));
}

return cleanedFeedbacks;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,26 @@ public class Assessment extends ArtemisConnectionHolder {
private final Locale studentLocale;

public Assessment(ResultDTO result, GradingConfig config, ProgrammingSubmission programmingSubmission, int correctionRound)
throws AnnotationMappingException {
throws AnnotationMappingException, ArtemisNetworkException {
this(result, config, programmingSubmission, correctionRound, Locale.GERMANY);
}

public Assessment(ResultDTO result, GradingConfig config, ProgrammingSubmission programmingSubmission, int correctionRound, Locale studentLocale)
throws AnnotationMappingException {
throws AnnotationMappingException, ArtemisNetworkException {
super(programmingSubmission);
this.programmingSubmission = programmingSubmission;
this.config = config;
this.correctionRound = correctionRound;
this.studentLocale = studentLocale;

// ensure that the feedbacks are fetched (some api endpoints do not return them)
// and for long feedbacks, we need to fetch the detailed feedbacks
var feedbacks = ResultDTO.fetchDetailedFeedbacks(this.getConnection().getClient(), result.id(), programmingSubmission.getParticipationId(),
result.feedbacks());

// Unpack the result
this.annotations = MetaFeedbackMapper.parseMetaFeedbacks(result.feedbacks(), config);
this.testResults = result.feedbacks().stream().filter(f -> f.type() == FeedbackType.AUTOMATIC).map(TestResult::new).toList();
this.annotations = MetaFeedbackMapper.parseMetaFeedbacks(feedbacks, config);
this.testResults = feedbacks.stream().filter(f -> f.type() == FeedbackType.AUTOMATIC).map(TestResult::new).toList();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ public String getTitle() {
return this.dto.title();
}

public String getShortName() {
return this.dto.shortName();
}

/**
* Gets all programming exercises of this course. The result is fetched lazily
* and then cached.
Expand Down Expand Up @@ -80,6 +84,10 @@ public int fetchLockedSubmissionCount() throws ArtemisNetworkException {
return CourseDTO.fetchLockedSubmissions(this.getConnection().getClient(), this.getId()).size();
}

public int getNumberOfStudents() {
return this.dto.numberOfStudents();
}

@Override
public String toString() {
return this.getTitle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ public boolean hasSecondCorrectionRound() {
* Fetches all submissions for this exercise. This may fetch *many* submissions,
* and does not cache the result, so be careful.
*
* @param correctionRound The correction round to fetch submissions for
* @param onlyOwn Whether to only fetch submissions that the current
* user has assessed
* @param correctionRound The correction round to fetch submissions for
* @param filterAssessedByTutor Whether to only fetch submissions that the
* current user has assessed
* @return a list of submissions
* @throws ArtemisNetworkException if the request fails
*/
public List<ProgrammingSubmission> fetchSubmissions(int correctionRound, boolean onlyOwn) throws ArtemisNetworkException {
return ProgrammingSubmissionDTO.fetchAll(this.getConnection().getClient(), this.getId(), correctionRound, onlyOwn).stream()
public List<ProgrammingSubmission> fetchSubmissions(int correctionRound, boolean filterAssessedByTutor) throws ArtemisNetworkException {
return ProgrammingSubmissionDTO.fetchAll(this.getConnection().getClient(), this.getId(), correctionRound, filterAssessedByTutor).stream()
.map(dto -> new ProgrammingSubmission(dto, this, correctionRound)).toList();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,32 @@ public boolean isSubmitted() {
return assessmentType == AssessmentType.MANUAL || assessmentType == AssessmentType.SEMI_AUTOMATIC;
}

/**
* Opens the assessment for this submission.
* <p>
* If the submission has not been assessed by you, you might not be able to
* change the assessment.
*
* @param config the config for the exercise
* @return the assessment if there are results for this submission
* @throws AnnotationMappingException If the annotations that were already
* present could not be mapped given the
* gradingConfig
*/
public Optional<Assessment> openAssessment(GradingConfig config) throws AnnotationMappingException, ArtemisNetworkException {
ResultDTO resultDTO = this.getRelevantResult().orElse(null);

if (resultDTO != null) {
return Optional.of(new Assessment(resultDTO, config, this, this.correctionRound));
}

return Optional.empty();
}

public boolean isBuildFailed() {
return this.dto.buildFailed();
}

@Override
public boolean equals(Object o) {
if (this == o)
Expand All @@ -146,7 +172,7 @@ private Optional<ResultDTO> getRelevantResult() {
return Optional.of(this.dto.results().get(0));
} else {
// More than one result, so probably multiple correction rounds
return Optional.of(this.dto.results().get(this.correctionRound));
return Optional.of(this.dto.nonAutomaticResults().get(this.correctionRound));
}
}
}
10 changes: 10 additions & 0 deletions src/main/java/edu/kit/kastel/sdq/artemis4j/grading/TestResult.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* Licensed under EPL-2.0 2024. */
package edu.kit.kastel.sdq.artemis4j.grading;

import java.util.Optional;

import edu.kit.kastel.sdq.artemis4j.client.FeedbackDTO;
import edu.kit.kastel.sdq.artemis4j.client.FeedbackType;

Expand All @@ -25,10 +27,18 @@ public double getPoints() {
return this.dto.credits();
}

public FeedbackType getFeedbackType() {
return this.dto.type();
}

protected FeedbackDTO getDto() {
return this.dto;
}

public Optional<Boolean> getPositive() {
return Optional.ofNullable(this.dto.positive());
}

@Override
public String toString() {
return "%s: %.3fP".formatted(this.getTestName(), this.getPoints());
Expand Down
28 changes: 28 additions & 0 deletions src/test/java/edu/kit/kastel/sdq/artemis4j/End2EndTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,32 @@ void testExportImport() throws ArtemisClientException {
this.assessment.importAssessment(exportedAssessment);
Assertions.assertEquals(oldAnnotations, this.assessment.getAnnotations());
}

@Test
void testAssessmentFetchesFeedbacks() throws ArtemisClientException {
// This test fetches all submissions of an exercise and checks that the
// assessment
// contains the previously added feedback.

// Create an annotation (feedback) in the submission:
MistakeType mistakeType = this.gradingConfig.getMistakeTypes().get(1);

this.assessment.addPredefinedAnnotation(mistakeType, "src/edu/kit/informatik/BubbleSort.java", 1, 2, null);
this.assessment.submit();

ProgrammingSubmission updatedSubmission = null;
// find the programming submission that was just assessed in all submissions of
// the exercise:
for (ProgrammingSubmission submission : this.exercise.fetchSubmissions()) {
if (submission.getId() == this.programmingSubmission.getId()) {
updatedSubmission = programmingSubmission;
break;
}
}

Assertions.assertEquals(this.programmingSubmission, updatedSubmission);

Assessment newAssessment = updatedSubmission.openAssessment(this.gradingConfig).orElseThrow();
Assertions.assertEquals(1, newAssessment.getAnnotations().size());
}
}

0 comments on commit 05e30fd

Please sign in to comment.