Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
399ebb8
Modeling exercises: Use DTOs for modeling exercise endpoints
WoH Jul 3, 2026
9a73474
Modeling exercises: Use DTOs for modeling assessment endpoints
WoH Jul 3, 2026
7a80a98
Modeling exercises: Use DTOs for modeling submission endpoints
WoH Jul 3, 2026
65aa8dc
Modeling exercises: Tighten entity-usage architecture thresholds
WoH Jul 3, 2026
52e4596
Modeling exercises: Preserve duplicate grading instructions in DTO co…
WoH Jul 3, 2026
47bc888
Modeling exercises: Send flat DTOs from Playwright modeling helpers
WoH Jul 3, 2026
4ff7573
Modeling exercises: Fix lazy categories and masked-exam course resolu…
WoH Jul 3, 2026
7b28841
Modeling exercises: Test update and re-evaluate with explicit empty c…
WoH Jul 5, 2026
9157a48
modeling: fix DTO review findings
WoH Jul 8, 2026
a9d6dff
modeling: address remaining review comments
WoH Jul 8, 2026
b5eec43
Modeling exercises: reconcile rebase onto develop
WoH Jul 18, 2026
adfc359
Modeling exercises: preserve DTO contracts and batch feedback lookups
WoH Jul 20, 2026
7f13a4d
Modeling: batch-load grading instructions in feedbacksFromDtos
WoH Jul 20, 2026
f2f1200
Modeling: pin client response contracts in integration tests
WoH Jul 20, 2026
2730df9
Modeling: complete final integration test assertions
WoH Jul 20, 2026
54c61fa
Modeling: validate re-evaluation plagiarism config
WoH Jul 20, 2026
6c9998c
Modeling exercises: fetch the plagiarism config for the exercise deta…
WoH Jul 20, 2026
ea00621
Modeling exercises: drop the redundant example-submissions fetch path
WoH Jul 20, 2026
1c21b90
empty
matyasht Jul 30, 2026
d442ece
Modeling exercises: initialize exam course before mapping management …
WoH Jul 31, 2026
adf4be9
Modeling exercises: keep plagiarism config id stable on write-back
WoH Jul 31, 2026
2ce666d
Modeling exercises: send DTO import bodies in three import tests
WoH Jul 31, 2026
cad4c21
Modeling exercises: never trust the client-sent plagiarism config id
WoH Jul 31, 2026
495da5a
Modeling exercises: cover the merge-based second save on exam import
WoH Jul 31, 2026
a9a0465
Merge remote-tracking branch 'origin/develop' into chore/modeling-exe…
WoH Aug 2, 2026
9f2784f
Merge remote-tracking branch 'origin/develop' into chore/modeling-exe…
WoH Aug 10, 2026
0bfa29a
Modeling exercises: resolve the variant group without a sixth fetch path
WoH Aug 10, 2026
c581dda
Merge remote-tracking branch 'origin/develop' into chore/modeling-exe…
WoH Aug 10, 2026
3b518ba
Merge remote-tracking branch 'origin/develop' into chore/modeling-exe…
WoH Aug 10, 2026
5fcecf3
Modeling exercises: keep the submission discriminator on the example-…
WoH Aug 21, 2026
7b05cc3
Merge branch 'develop' into chore/modeling-exercises/modeling-module-…
FelixTJDietrich Aug 24, 2026
d592887
Modeling exercises: carry exerciseId on echoed results
WoH Aug 25, 2026
b4c8565
Modeling exercises: prove the example-submission revisit save end to end
WoH Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.tum.cit.aet.artemis.assessment.dto;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -12,8 +13,12 @@
import de.tum.cit.aet.artemis.assessment.domain.GradingCriterion;
import de.tum.cit.aet.artemis.assessment.domain.GradingInstruction;

// structuredGradingInstructions is a List, not a Set: GradingInstructionDTO is a record whose value equality spans all
// components including the nullable id, so two value-identical new instructions (id == null) would collapse in a Set on
// both mapping and Jackson deserialization. The entity path never collapses them (DomainObject.equals is false when an
// id is null), so a Set here would silently drop rubric rows. A List preserves every instruction.
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record GradingCriterionDTO(Long id, String title, Set<GradingInstructionDTO> structuredGradingInstructions) {
public record GradingCriterionDTO(Long id, String title, List<GradingInstructionDTO> structuredGradingInstructions) {

/**
* Convert GradingCriterion to GradingCriterionDTO. Used in the exercise DTOs for Athena.
Expand All @@ -22,8 +27,9 @@ public record GradingCriterionDTO(Long id, String title, Set<GradingInstructionD
* @return a GradingCriterionDTO based on the GradingCriterion
*/
public static GradingCriterionDTO of(@NotNull GradingCriterion gradingCriterion) {
return new GradingCriterionDTO(gradingCriterion.getId(), gradingCriterion.getTitle(),
gradingCriterion.getStructuredGradingInstructions().stream().map(GradingInstructionDTO::of).collect(Collectors.toSet()));
List<GradingInstructionDTO> instructions = gradingCriterion.getStructuredGradingInstructions() == null ? List.of()
: gradingCriterion.getStructuredGradingInstructions().stream().map(GradingInstructionDTO::of).toList();
return new GradingCriterionDTO(gradingCriterion.getId(), gradingCriterion.getTitle(), instructions);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record ResultDTO(Long id, ZonedDateTime completionDate, Boolean successful, Double score, Boolean rated, SubmissionDTO submission, ParticipationDTO participation,
List<FeedbackDTO> feedbacks, AssessmentType assessmentType, Boolean hasComplaint, Boolean exampleResult, UserNameDTO assessor, AssessmentNoteDTO assessmentNote)
implements Serializable {
List<FeedbackDTO> feedbacks, AssessmentType assessmentType, Boolean hasComplaint, Boolean exampleResult, UserNameDTO assessor, AssessmentNoteDTO assessmentNote,
Long exerciseId) implements Serializable {

/**
* Converts a Result into a ResultDTO.
Expand Down Expand Up @@ -52,7 +52,11 @@ public static ResultDTO of(Result result) {
assessorDTO = UserNameDTO.of(result.getAssessor());
}
AssessmentNoteDTO assessmentNoteDTO = AssessmentNoteDTO.of(result.getAssessmentNote());
// exerciseId is a non-null FK column on the result table (denormalized in #11459) that the entity payload always
// carried. Clients echo loaded results back into entity-typed endpoints (example-submission save, manual
// results), where the cascade merge writes the column back - without it on the wire the echo merges
// exercise_id = 0 and dies on the foreign-key constraint.
return new ResultDTO(result.getId(), result.getCompletionDate(), result.isSuccessful(), result.getScore(), result.isRated(), submissionDTO, participationDTO, feedbackDTOs,
result.getAssessmentType(), result.hasComplaint(), result.isExampleResult(), assessorDTO, assessmentNoteDTO);
result.getAssessmentType(), result.hasComplaint(), result.isExampleResult(), assessorDTO, assessmentNoteDTO, result.getExerciseId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,23 @@ public static ParticipationDTO of(Participation participation) {
public record ParticipationExerciseDTO(Long id, ExerciseType exerciseType, String type, AssessmentType assessmentType, ZonedDateTime dueDate, ZonedDateTime assessmentDueDate,
Double maxPoints, CourseDTO course) implements Serializable {

/**
* Maps an {@link Exercise} to a {@link ParticipationExerciseDTO}.
* <p>
* Student-facing endpoints mask exam exercises by stripping {@code exerciseGroup.exam} before mapping (the
* masked-exam state). In that state {@link Exercise#getCourseViaExerciseGroupOrCourseMember()} would dereference the
* now-missing exam and throw, so the course is resolved to {@code null} instead.
*
* @param exercise the exercise to convert (may be {@code null})
* @return the corresponding DTO, or {@code null} if the input was {@code null}
*/
@Nullable
public static ParticipationExerciseDTO of(Exercise exercise) {
return Optional.ofNullable(exercise).map(e -> new ParticipationExerciseDTO(e.getId(), e.getExerciseType(), e.getType(), e.getAssessmentType(), e.getDueDate(),
e.getAssessmentDueDate(), e.getMaxPoints(), CourseDTO.of(e.getCourseViaExerciseGroupOrCourseMember()))).orElse(null);
return Optional.ofNullable(exercise).map(e -> {
Course course = e.isExamExercise() && (e.getExerciseGroup() == null || e.getExerciseGroup().getExam() == null) ? null : e.getCourseViaExerciseGroupOrCourseMember();
return new ParticipationExerciseDTO(e.getId(), e.getExerciseType(), e.getType(), e.getAssessmentType(), e.getDueDate(), e.getAssessmentDueDate(), e.getMaxPoints(),
CourseDTO.of(course));
}).orElse(null);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package de.tum.cit.aet.artemis.modeling.dto;

import com.fasterxml.jackson.annotation.JsonInclude;

/**
* Input DTO mirroring the {@code ComplaintResponse} wire shape the client sends when updating a modeling assessment after a
* complaint. Only the fields the server needs are captured (the lock id, the response text and the complaint resolution
* decision); the controller reconstructs a transient {@code ComplaintResponse} from these for the shared
* assessment-update logic.
* <p>
* Bare {@code @JsonInclude} (no explicit value, i.e. Jackson's ALWAYS default) is used rather than {@code NON_EMPTY}: this is a request
* body and must round-trip empty/blank values unchanged.
*
* @param id the id of the (locked) complaint response
* @param responseText the tutor's response text
* @param complaint the complaint carrying the accept/reject decision
*/
@JsonInclude
public record ComplaintResponseRequestDTO(Long id, String responseText, ComplaintRequestDTO complaint) {

/**
* The nested complaint shape carrying the resolution decision.
*
* @param id the complaint id
* @param accepted whether the complaint was accepted
*/
@JsonInclude
public record ComplaintRequestDTO(Long id, Boolean accepted) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package de.tum.cit.aet.artemis.modeling.dto;

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Set;

import org.hibernate.Hibernate;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.assessment.dto.GradingCriterionDTO;
import de.tum.cit.aet.artemis.exercise.domain.DifficultyLevel;
import de.tum.cit.aet.artemis.exercise.domain.ExerciseMode;
import de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore;
import de.tum.cit.aet.artemis.exercise.dto.CompetencyLinksHolderDTO;
import de.tum.cit.aet.artemis.exercise.dto.TeamAssignmentConfigDTO;
import de.tum.cit.aet.artemis.lecture.dto.CompetencyLinkDTO;
import de.tum.cit.aet.artemis.modeling.domain.DiagramType;
import de.tum.cit.aet.artemis.modeling.domain.ModelingExercise;
import de.tum.cit.aet.artemis.modeling.util.ModelingDtoCollections;
import de.tum.cit.aet.artemis.plagiarism.dto.PlagiarismDetectionConfigDTO;

/**
* Input DTO for importing a modeling exercise.
* Superset of {@link UpdateModelingExerciseDTO} with the additional configuration needed during import.
* Dumb DTO: only scalars, enums, date/time values, and nested DTOs. The controller builds the entity from this payload.
*/
@JsonInclude
public record ImportModelingExerciseDTO(Long id, String title, String channelName, String shortName, String problemStatement, Set<String> categories, DifficultyLevel difficulty,
ExerciseMode mode, Double maxPoints, Double bonusPoints, IncludedInOverallScore includedInOverallScore, Boolean allowComplaintsForAutomaticAssessments,
Boolean allowFeedbackRequests, Boolean presentationScoreEnabled, Boolean secondCorrectionEnabled, String feedbackSuggestionModule, String gradingInstructions,
ZonedDateTime releaseDate, ZonedDateTime startDate, ZonedDateTime dueDate, ZonedDateTime assessmentDueDate, ZonedDateTime exampleSolutionPublicationDate,
DiagramType diagramType, String exampleSolutionModel, String exampleSolutionExplanation, Long courseId, Long exerciseGroupId, TeamAssignmentConfigDTO teamAssignmentConfig,
PlagiarismDetectionConfigDTO plagiarismDetectionConfig, List<GradingCriterionDTO> gradingCriteria, Set<CompetencyLinkDTO> competencyLinks)
implements CompetencyLinksHolderDTO {

/**
* Creates an ImportModelingExerciseDTO from the given source/target modeling exercise (used for tests and import flows).
*
* @param exercise the modeling exercise to convert
* @return the corresponding import DTO, or {@code null} if the exercise is {@code null}
*/
public static ImportModelingExerciseDTO of(ModelingExercise exercise) {
if (exercise == null) {
return null;
}
// Only a directly-attached course yields a courseId (isCourseExercise() checks the direct course field), so an exam
// exercise yields courseId == null, keeping the course/exerciseGroup exclusivity intact for import requests.
Long courseId = exercise.isCourseExercise() ? exercise.getCourseViaExerciseGroupOrCourseMember().getId() : null;
Long exerciseGroupId = exercise.getExerciseGroup() != null ? exercise.getExerciseGroup().getId() : null;

List<GradingCriterionDTO> gradingCriterionDTOs = ModelingDtoCollections.listFromInitializedSet(exercise.getGradingCriteria(), GradingCriterionDTO::of);
Set<CompetencyLinkDTO> competencyLinkDTOs = ModelingDtoCollections.setFromInitializedSet(exercise.getCompetencyLinks(), CompetencyLinkDTO::of);
TeamAssignmentConfigDTO teamAssignmentConfig = Hibernate.isInitialized(exercise.getTeamAssignmentConfig()) ? TeamAssignmentConfigDTO.of(exercise.getTeamAssignmentConfig())
: null;
PlagiarismDetectionConfigDTO plagiarismDetectionConfig = Hibernate.isInitialized(exercise.getPlagiarismDetectionConfig())
? PlagiarismDetectionConfigDTO.of(exercise.getPlagiarismDetectionConfig())
: null;

// categories is a LAZY @ElementCollection; copy it (guarded) so the DTO never holds the live Hibernate persistent
// set (a DTO toString via LoggingAspect would otherwise trigger a LazyInitializationException on Exercise.categories).
Set<String> categories = ModelingDtoCollections.copyInitializedSet(exercise.getCategories());

return new ImportModelingExerciseDTO(exercise.getId(), exercise.getTitle(), exercise.getChannelName(), exercise.getShortName(), exercise.getProblemStatement(), categories,
exercise.getDifficulty(), exercise.getMode(), exercise.getMaxPoints(), exercise.getBonusPoints(), exercise.getIncludedInOverallScore(),
exercise.getAllowComplaintsForAutomaticAssessments(), exercise.getAllowFeedbackRequests(), exercise.getPresentationScoreEnabled(),
exercise.getSecondCorrectionEnabled(), exercise.getFeedbackSuggestionModule(), exercise.getGradingInstructions(), exercise.getReleaseDate(),
exercise.getStartDate(), exercise.getDueDate(), exercise.getAssessmentDueDate(), exercise.getExampleSolutionPublicationDate(), exercise.getDiagramType(),
exercise.getExampleSolutionModel(), exercise.getExampleSolutionExplanation(), courseId, exerciseGroupId, teamAssignmentConfig, plagiarismDetectionConfig,
gradingCriterionDTOs, competencyLinkDTOs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.assessment.domain.Feedback;
import de.tum.cit.aet.artemis.assessment.dto.FeedbackDTO;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record ModelingAssessmentDTO(List<Feedback> feedbacks, String assessmentNote) {
/**
* Input DTO for saving or submitting a modeling assessment.
* <p>
* The controller maps {@link FeedbackDTO} to {@code Feedback} before persisting. {@code @JsonInclude(ALWAYS)} is explicit
* (matching Jackson's default) rather than {@code NON_EMPTY}: this is a request body and an empty {@code feedbacks} list
* must stay an empty array on the wire (the save path clears the existing feedback when an empty list is sent, mirroring
* the previous behavior where the list was never null).
*
* @param feedbacks the feedback items of the assessment
* @param assessmentNote the optional assessment note attached to the result
*/
@JsonInclude
public record ModelingAssessmentDTO(List<FeedbackDTO> feedbacks, String assessmentNote) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.tum.cit.aet.artemis.modeling.dto;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.assessment.dto.FeedbackDTO;

/**
* Input DTO for updating a modeling assessment after a complaint.
* <p>
* This DTO intentionally does NOT implement {@code AssessmentUpdateBaseDTO}: that interface requires entity-typed
* {@code List<Feedback> feedbacks()} and {@code ComplaintResponse complaintResponse()}, which are incompatible with the dumb
* DTO component types ({@link FeedbackDTO} / {@link ComplaintResponseRequestDTO}). The controller adapts these DTOs to the
* entity types before delegating to the shared assessment-update logic. The {@link ComplaintResponseRequestDTO} mirrors the
* {@code ComplaintResponse} wire shape the client sends (a nested complaint carrying the accept/reject decision).
* Bare {@code @JsonInclude} (no explicit value, i.e. Jackson's ALWAYS default) is used rather than {@code NON_EMPTY}: this is a request
* body and an empty {@code feedbacks} list must stay an empty array on the wire.
*
* @param feedbacks the updated feedback items of the assessment
* @param complaintResponse the response to the complaint carrying the resolution decision
* @param assessmentNote the optional assessment note attached to the result
*/
@JsonInclude
public record ModelingAssessmentUpdateDTO(List<FeedbackDTO> feedbacks, ComplaintResponseRequestDTO complaintResponse, String assessmentNote) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package de.tum.cit.aet.artemis.modeling.dto;

import java.io.Serializable;
import java.util.List;

import org.hibernate.Hibernate;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

import de.tum.cit.aet.artemis.assessment.domain.ExampleSubmission;
import de.tum.cit.aet.artemis.assessment.dto.ResultDTO;
import de.tum.cit.aet.artemis.modeling.domain.ModelingSubmission;

/**
* Read DTO for an {@link ExampleSubmission} of a modeling exercise, as exposed on the single modeling-exercise detail
* endpoint. Carries the example submission with a minimal projection of its (example) submission so the example
* submission management page can render the diagram size and the "example assessment created" marker.
* <p>
* The nested submission projection is intentionally purpose-built and minimal (it does not reference the not-yet-existing
* {@code ModelingSubmissionResponseDTO}): the management page reads {@code submission.model} (to compute the diagram
* size) and {@code submission.results[*].exampleResult} (to flag whether an example assessment exists).
*
* @param id the example submission id
* @param usedForTutorial whether this example submission is used for the tutorial
* @param submission the minimal example modeling-submission projection
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record ModelingExampleSubmissionDTO(Long id, boolean usedForTutorial, ExampleModelingSubmissionDTO submission) implements Serializable {

/**
* Minimal projection of the example {@link ModelingSubmission} the example-submission management page reads.
*
* @param id the submission id
* @param model the UML model (diagram-size computation on the management page)
* @param explanationText the explanation text of the submission
* @param submitted whether the submission was submitted
* @param results the (example) results; the page reads {@code results[*].exampleResult}
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record ExampleModelingSubmissionDTO(Long id, String model, String explanationText, Boolean submitted, List<ResultDTO> results) implements Serializable {

/**
* The example-submission edit page echoes the exercise detail response back into its save PUT, where this
* projection is deserialized into the abstract {@link de.tum.cit.aet.artemis.exercise.domain.Submission}.
* That type resolves its subtype from this discriminator, so it must stay on the wire (the entity payload
* carried it via {@code @JsonTypeInfo}); without it the PUT fails as unreadable.
*
* @return the constant submission type discriminator
*/
@JsonProperty("submissionExerciseType")
public String submissionExerciseType() {
return "modeling";
}
}

/**
* Converts an {@link ExampleSubmission} into a {@link ModelingExampleSubmissionDTO}.
*
* @param exampleSubmission the example submission to convert (may be {@code null})
* @return the converted DTO, or {@code null} if the input was {@code null}
*/
public static ModelingExampleSubmissionDTO of(ExampleSubmission exampleSubmission) {
if (exampleSubmission == null) {
return null;
}
ExampleModelingSubmissionDTO submission = null;
if (exampleSubmission.getSubmission() instanceof ModelingSubmission modelingSubmission) {
List<ResultDTO> results = Hibernate.isInitialized(modelingSubmission.getResults()) && modelingSubmission.getResults() != null
? modelingSubmission.getResults().stream().map(ResultDTO::of).toList()
: null;
submission = new ExampleModelingSubmissionDTO(modelingSubmission.getId(), modelingSubmission.getModel(), modelingSubmission.getExplanationText(),
modelingSubmission.isSubmitted(), results);
}
return new ModelingExampleSubmissionDTO(exampleSubmission.getId(), exampleSubmission.isUsedForTutorial(), submission);
}
}
Loading
Loading