diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java index 823c61800539..c539e3b183ab 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java @@ -1036,7 +1036,7 @@ private void validateExamExerciseIncludedInScoreCompletely() { * Just setting the collections to {@code null} breaks the automatic orphan removal and change detection in the database. */ public void disconnectRelatedEntities() { - Stream.of(competencies, teams, gradingCriteria, studentParticipations, tutorParticipations, exampleSubmissions, attachments, posts, plagiarismCases) - .filter(Objects::nonNull).forEach(Collection::clear); + Stream.of(teams, gradingCriteria, studentParticipations, tutorParticipations, exampleSubmissions, attachments, posts, plagiarismCases).filter(Objects::nonNull) + .forEach(Collection::clear); } } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/AttachmentUnitRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/AttachmentUnitRepository.java index 4869b15aad8c..28c0a60f63c4 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/AttachmentUnitRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/AttachmentUnitRepository.java @@ -58,7 +58,8 @@ default List findAllByLectureIdAndAttachmentTypeElseThrow(Long l SELECT attachmentUnit FROM AttachmentUnit attachmentUnit LEFT JOIN FETCH attachmentUnit.slides slides + LEFT JOIN FETCH attachmentUnit.competencies WHERE attachmentUnit.id = :attachmentUnitId """) - AttachmentUnit findOneWithSlides(@Param("attachmentUnitId") long attachmentUnitId); + AttachmentUnit findOneWithSlidesAndCompetencies(@Param("attachmentUnitId") long attachmentUnitId); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java index 43fbf79886fe..8de27dc05aec 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CompetencyRepository.java @@ -17,7 +17,6 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.competency.Competency; -import de.tum.in.www1.artemis.domain.competency.LearningPath; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; /** @@ -118,14 +117,6 @@ Page findForImport(@Param("partialTitle") String partialTitle, @Para @Cacheable(cacheNames = "competencyTitle", key = "#competencyId", unless = "#result == null") String getCompetencyTitle(@Param("competencyId") long competencyId); - @Query(""" - SELECT c - FROM Competency c - LEFT JOIN FETCH c.learningPaths lp - WHERE lp = :learningPath - """) - Set findAllByLearningPath(@Param("learningPath") LearningPath learningPath); - default Competency findByIdWithExercisesElseThrow(long competencyId) { return getValueElseThrow(findByIdWithExercises(competencyId), competencyId); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java index 0774427eb287..fbc8f21b7c5a 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseCompetencyRepository.java @@ -7,8 +7,11 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.LearningObject; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.competency.CourseCompetency; +import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; import de.tum.in.www1.artemis.web.rest.dto.metrics.CompetencyExerciseMasteryCalculationDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; @@ -97,6 +100,22 @@ GROUP BY ex.maxPoints, ex.difficulty, TYPE(ex), sS.lastScore, tS.lastScore, sS.l """) Optional findByIdWithExercisesAndLectureUnitsBidirectional(@Param("competencyId") long competencyId); + @Query(""" + SELECT c.id + FROM CourseCompetency c + LEFT JOIN c.exercises ex + WHERE :exercise = ex + """) + Set findAllIdsByExercise(@Param("exercise") Exercise exercise); + + @Query(""" + SELECT c.id + FROM CourseCompetency c + LEFT JOIN c.lectureUnits lu + WHERE :lectureUnit = lu + """) + Set findAllIdsByLectureUnit(@Param("lectureUnit") LectureUnit lectureUnit); + /** * Finds a list of competencies by id and verifies that the user is at least editor in the respective courses. * If any of the competencies are not accessible, throws a {@link EntityNotFoundException} @@ -139,5 +158,19 @@ default CourseCompetency findByIdWithExercisesAndLectureUnitsBidirectionalElseTh return getValueElseThrow(findByIdWithExercisesAndLectureUnitsBidirectional(competencyId), competencyId); } + /** + * Finds the set of ids of course competencies that are linked to a given learning object + * + * @param learningObject the learning object to find the course competencies for + * @return the set of ids of course competencies linked to the learning object + */ + default Set findAllIdsByLearningObject(LearningObject learningObject) { + return switch (learningObject) { + case LectureUnit lectureUnit -> findAllIdsByLectureUnit(lectureUnit); + case Exercise exercise -> findAllIdsByExercise(exercise); + default -> throw new IllegalArgumentException("Unknown LearningObject type: " + learningObject.getClass()); + }; + } + List findByCourseIdOrderById(long courseId); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/FileUploadExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/FileUploadExerciseRepository.java index bc9c5a5814b1..1c0e7617f0cf 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/FileUploadExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/FileUploadExerciseRepository.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.Optional; +import jakarta.validation.constraints.NotNull; + import org.springframework.context.annotation.Profile; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -30,6 +32,14 @@ public interface FileUploadExerciseRepository extends ArtemisJpaRepository findByCourseIdWithCategories(@Param("courseId") Long courseId); + @EntityGraph(type = LOAD, attributePaths = { "competencies" }) + Optional findWithEagerCompetenciesById(Long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "teamAssignmentConfig", "categories", "competencies" }) Optional findWithEagerTeamAssignmentConfigAndCategoriesAndCompetenciesById(Long exerciseId); + + @NotNull + default FileUploadExercise findWithEagerCompetenciesByIdElseThrow(Long exerciseId) { + return getValueElseThrow(findWithEagerCompetenciesById(exerciseId), exerciseId); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitCompletionRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitCompletionRepository.java index 0b4afde4f274..b7c9349479a0 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitCompletionRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitCompletionRepository.java @@ -11,6 +11,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnitCompletion; import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository; @@ -44,4 +45,12 @@ SELECT COUNT(lectureUnitCompletion) AND lectureUnitCompletion.user.id = :userId """) int countByLectureUnitIdsAndUserId(@Param("lectureUnitIds") Collection lectureUnitIds, @Param("userId") Long userId); + + @Query(""" + SELECT user + FROM LectureUnitCompletion lectureUnitCompletion + LEFT JOIN lectureUnitCompletion.user user + WHERE lectureUnitCompletion.lectureUnit = :lectureUnit + """) + Set findCompletedUsersForLectureUnit(@Param("lectureUnit") LectureUnit lectureUnit); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java index bcfaf7c4e008..dc136ef9de74 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/LectureUnitRepository.java @@ -21,16 +21,6 @@ @Repository public interface LectureUnitRepository extends ArtemisJpaRepository { - @Query(""" - SELECT lu - FROM LectureUnit lu - LEFT JOIN FETCH lu.competencies - LEFT JOIN FETCH lu.exercise exercise - LEFT JOIN FETCH exercise.competencies - WHERE lu.id = :lectureUnitId - """) - Optional findWithCompetenciesById(@Param("lectureUnitId") Long lectureUnitId); - @Query(""" SELECT lu FROM LectureUnit lu diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ModelingExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ModelingExerciseRepository.java index d867cd2536a2..1aa7300720ea 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ModelingExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ModelingExerciseRepository.java @@ -42,6 +42,9 @@ public interface ModelingExerciseRepository extends ArtemisJpaRepository findWithEagerExampleSubmissionsAndCompetenciesAndPlagiarismDetectionConfigById(Long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "competencies" }) + Optional findWithEagerCompetenciesById(Long exerciseId); + @Query(""" SELECT modelingExercise FROM ModelingExercise modelingExercise @@ -112,4 +115,9 @@ default ModelingExercise findByIdWithExampleSubmissionsAndResultsAndPlagiarismDe default ModelingExercise findByIdWithStudentParticipationsSubmissionsResultsElseThrow(long exerciseId) { return findWithStudentParticipationsSubmissionsResultsById(exerciseId).orElseThrow(() -> new EntityNotFoundException("Modeling Exercise", exerciseId)); } + + @NotNull + default ModelingExercise findWithEagerCompetenciesByIdElseThrow(long exerciseId) { + return getValueElseThrow(findWithEagerCompetenciesById(exerciseId), exerciseId); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/OnlineUnitRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/OnlineUnitRepository.java index 81b7b9d05cfb..a62b8c4c7dda 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/OnlineUnitRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/OnlineUnitRepository.java @@ -2,7 +2,13 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import java.util.Optional; + +import jakarta.validation.constraints.NotNull; + import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import de.tum.in.www1.artemis.domain.lecture.OnlineUnit; @@ -15,4 +21,16 @@ @Repository public interface OnlineUnitRepository extends ArtemisJpaRepository { + @Query(""" + SELECT ou + FROM OnlineUnit ou + LEFT JOIN FETCH ou.competencies + WHERE ou.id = :onlineUnitId + """) + Optional findByIdWithCompetencies(@Param("onlineUnitId") long onlineUnitId); + + @NotNull + default OnlineUnit findByIdWithCompetenciesElseThrow(long onlineUnitId) { + return getValueElseThrow(findByIdWithCompetencies(onlineUnitId), onlineUnitId); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java index 41cd1ffb5ba5..3e14ff40794f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ProgrammingExerciseRepository.java @@ -92,6 +92,9 @@ public interface ProgrammingExerciseRepository extends DynamicSpecificationRepos @EntityGraph(type = LOAD, attributePaths = "auxiliaryRepositories") Optional findWithAuxiliaryRepositoriesById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "auxiliaryRepositories", "competencies" }) + Optional findWithAuxiliaryRepositoriesAndCompetenciesById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = "submissionPolicy") Optional findWithSubmissionPolicyById(long exerciseId); @@ -520,6 +523,17 @@ default ProgrammingExercise findByIdWithAuxiliaryRepositoriesElseThrow(long prog return findWithAuxiliaryRepositoriesById(programmingExerciseId).orElseThrow(() -> new EntityNotFoundException("Programming Exercise", programmingExerciseId)); } + /** + * Find a programming exercise with auxiliary repositories and competencies by its id and throw an {@link EntityNotFoundException} if it cannot be found + * + * @param programmingExerciseId of the programming exercise. + * @return The programming exercise related to the given id + */ + @NotNull + default ProgrammingExercise findByIdWithAuxiliaryRepositoriesAndCompetenciesElseThrow(long programmingExerciseId) throws EntityNotFoundException { + return getValueElseThrow(findWithAuxiliaryRepositoriesAndCompetenciesById(programmingExerciseId), programmingExerciseId); + } + /** * Find a programming exercise with the submission policy by its id and throw an EntityNotFoundException if it cannot be found * diff --git a/src/main/java/de/tum/in/www1/artemis/repository/QuizExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/QuizExerciseRepository.java index e1b06708f1bb..5f19ce691a1d 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/QuizExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/QuizExerciseRepository.java @@ -76,14 +76,12 @@ public interface QuizExerciseRepository extends ArtemisJpaRepository findWithEagerQuestionsById(Long quizExerciseId); + @EntityGraph(type = LOAD, attributePaths = { "quizQuestions", "competencies" }) + Optional findWithEagerQuestionsAndCompetenciesById(Long quizExerciseId); + @EntityGraph(type = LOAD, attributePaths = { "quizBatches" }) Optional findWithEagerBatchesById(Long quizExerciseId); - @NotNull - default QuizExercise findWithEagerQuestionsByIdOrElseThrow(Long quizExerciseId) { - return findWithEagerQuestionsById(quizExerciseId).orElseThrow(() -> new EntityNotFoundException("QuizExercise", quizExerciseId)); - } - @NotNull default QuizExercise findWithEagerBatchesByIdOrElseThrow(Long quizExerciseId) { return findWithEagerBatchesById(quizExerciseId).orElseThrow(() -> new EntityNotFoundException("QuizExercise", quizExerciseId)); @@ -111,6 +109,17 @@ default QuizExercise findByIdWithQuestionsElseThrow(Long quizExerciseId) { return findWithEagerQuestionsById(quizExerciseId).orElseThrow(() -> new EntityNotFoundException("Quiz Exercise", quizExerciseId)); } + /** + * Get one quiz exercise by id and eagerly load questions + * + * @param quizExerciseId the id of the entity + * @return the entity + */ + @NotNull + default QuizExercise findByIdWithQuestionsAndCompetenciesElseThrow(Long quizExerciseId) { + return getValueElseThrow(findWithEagerQuestionsAndCompetenciesById(quizExerciseId), quizExerciseId); + } + /** * Get one quiz exercise by id and eagerly load batches * diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentScoreRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentScoreRepository.java index 04da26cf0a11..d7afb0bb7acd 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentScoreRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentScoreRepository.java @@ -58,6 +58,14 @@ public interface StudentScoreRepository extends ArtemisJpaRepository findAllByExerciseAndUserWithEagerExercise(@Param("exercises") Set exercises, @Param("user") User user); + @Query(""" + SELECT stud + FROM StudentScore s + LEFT JOIN s.user stud + WHERE s.exercise = :exercise + """) + Set findAllUsersWithScoresByExercise(@Param("exercise") Exercise exercise); + @Transactional // ok because of delete @Modifying void deleteByExerciseAndUser(Exercise exercise, User user); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/TeamScoreRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/TeamScoreRepository.java index e3b42a9c57a1..83e7146866ea 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/TeamScoreRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/TeamScoreRepository.java @@ -63,6 +63,15 @@ public interface TeamScoreRepository extends ArtemisJpaRepository findAllByExercisesAndUser(@Param("exercises") Set exercises, @Param("user") User user); + @Query(""" + SELECT studs + FROM TeamScore s + LEFT JOIN s.team t + LEFT JOIN t.students studs + WHERE s.exercise = :exercise + """) + Set findAllUsersWithScoresByExercise(@Param("exercise") Exercise exercise); + @Transactional // ok because of delete @Modifying void deleteByExerciseAndTeam(Exercise exercise, Team team); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/TextExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/TextExerciseRepository.java index 23466f1280c0..5b09083b2f5d 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/TextExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/TextExerciseRepository.java @@ -34,6 +34,9 @@ public interface TextExerciseRepository extends ArtemisJpaRepository findByCourseIdWithCategories(@Param("courseId") long courseId); + @EntityGraph(type = LOAD, attributePaths = { "competencies" }) + Optional findWithEagerCompetenciesById(long exerciseId); + @EntityGraph(type = LOAD, attributePaths = { "teamAssignmentConfig", "categories", "competencies" }) Optional findWithEagerTeamAssignmentConfigAndCategoriesAndCompetenciesById(long exerciseId); @@ -65,6 +68,11 @@ default TextExercise findWithGradingCriteriaByIdElseThrow(long exerciseId) { return findWithGradingCriteriaById(exerciseId).orElseThrow(() -> new EntityNotFoundException("Text Exercise", exerciseId)); } + @NotNull + default TextExercise findWithEagerCompetenciesByIdElseThrow(long exerciseId) { + return getValueElseThrow(findWithEagerCompetenciesById(exerciseId), exerciseId); + } + @NotNull default TextExercise findByIdWithExampleSubmissionsAndResultsElseThrow(long exerciseId) { return findWithExampleSubmissionsAndResultsById(exerciseId).orElseThrow(() -> new EntityNotFoundException("Text Exercise", exerciseId)); diff --git a/src/main/java/de/tum/in/www1/artemis/repository/TextUnitRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/TextUnitRepository.java index 5d3b1491437e..7e346d1179b6 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/TextUnitRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/TextUnitRepository.java @@ -27,13 +27,4 @@ public interface TextUnitRepository extends ArtemisJpaRepository """) Optional findByIdWithCompetencies(@Param("textUnitId") Long textUnitId); - @Query(""" - SELECT tu - FROM TextUnit tu - LEFT JOIN FETCH tu.competencies c - LEFT JOIN FETCH c.lectureUnits - WHERE tu.id = :textUnitId - """) - Optional findByIdWithCompetenciesBidirectional(@Param("textUnitId") Long textUnitId); - } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java index 89e36682bccb..6d7d094c6ec2 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java @@ -181,6 +181,16 @@ OR LOWER(user.login) = LOWER(:searchInput) @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Set findAllWithGroupsAndAuthoritiesByIsDeletedIsFalseAndGroupsContains(String groupName); + @Query(""" + SELECT DISTINCT user + FROM User user + LEFT JOIN FETCH user.groups userGroup + LEFT JOIN FETCH user.authorities userAuthority + WHERE user.isDeleted = FALSE + AND userGroup IN :groupNames + """) + Set findAllWithGroupsAndAuthoritiesByIsDeletedIsFalseAndGroupsContains(@Param("groupNames") Set groupNames); + Set findAllByIsDeletedIsFalseAndGroupsContains(String groupName); @Query(""" @@ -760,17 +770,6 @@ default User getUserWithGroupsAndAuthorities(@NotNull String username) { return unwrapOptionalUser(user, username); } - /** - * Get user with authorities with the username (i.e. user.getLogin() or principal.getName()) - * - * @param username the username of the user who should be retrieved from the database - * @return the user that belongs to the given principal with eagerly loaded authorities - */ - default User getUserWithAuthorities(@NotNull String username) { - Optional user = findOneWithAuthoritiesByLogin(username); - return unwrapOptionalUser(user, username); - } - /** * Finds a single user with groups and authorities using the registration number * @@ -877,6 +876,17 @@ default Set getInstructors(Course course) { return findAllWithGroupsAndAuthoritiesByIsDeletedIsFalseAndGroupsContains(course.getInstructorGroupName()); } + /** + * Get all users for a given course + * + * @param course The course for which to fetch all users + * @return all users in the course + */ + default Set getUsersInCourse(Course course) { + Set groupNames = Set.of(course.getStudentGroupName(), course.getTeachingAssistantGroupName(), course.getEditorGroupName(), course.getInstructorGroupName()); + return findAllWithGroupsAndAuthoritiesByIsDeletedIsFalseAndGroupsContains(groupNames); + } + /** * Finds all users that are part of the specified group, but are not contained in the collection of excluded users * diff --git a/src/main/java/de/tum/in/www1/artemis/repository/VideoUnitRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/VideoUnitRepository.java index 6fa8229fb9b3..e936f10fdd8d 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/VideoUnitRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/VideoUnitRepository.java @@ -2,7 +2,13 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import java.util.Optional; + +import jakarta.validation.constraints.NotNull; + import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import de.tum.in.www1.artemis.domain.lecture.VideoUnit; @@ -15,4 +21,16 @@ @Repository public interface VideoUnitRepository extends ArtemisJpaRepository { + @Query(""" + SELECT vu + FROM VideoUnit vu + LEFT JOIN FETCH vu.competencies + WHERE vu.id = :videoUnitId + """) + Optional findByIdWithCompetencies(@Param("videoUnitId") long videoUnitId); + + @NotNull + default VideoUnit findByIdWithCompetenciesElseThrow(long videoUnitId) { + return getValueElseThrow(findByIdWithCompetencies(videoUnitId), videoUnitId); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/AttachmentUnitService.java b/src/main/java/de/tum/in/www1/artemis/service/AttachmentUnitService.java index 996873825eb6..d0a87d0b6994 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/AttachmentUnitService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/AttachmentUnitService.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; import org.apache.commons.io.FilenameUtils; import org.springframework.context.annotation.Profile; @@ -16,12 +17,14 @@ import de.tum.in.www1.artemis.domain.Attachment; import de.tum.in.www1.artemis.domain.Lecture; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; import de.tum.in.www1.artemis.domain.lecture.Slide; import de.tum.in.www1.artemis.repository.AttachmentRepository; import de.tum.in.www1.artemis.repository.AttachmentUnitRepository; import de.tum.in.www1.artemis.repository.SlideRepository; import de.tum.in.www1.artemis.repository.iris.IrisSettingsRepository; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.connectors.pyris.PyrisWebhookService; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @@ -43,9 +46,11 @@ public class AttachmentUnitService { private final Optional irisSettingsRepository; + private final CompetencyProgressService competencyProgressService; + public AttachmentUnitService(SlideRepository slideRepository, SlideSplitterService slideSplitterService, AttachmentUnitRepository attachmentUnitRepository, AttachmentRepository attachmentRepository, FileService fileService, Optional pyrisWebhookService, - Optional irisSettingsRepository) { + Optional irisSettingsRepository, CompetencyProgressService competencyProgressService) { this.attachmentUnitRepository = attachmentUnitRepository; this.attachmentRepository = attachmentRepository; this.fileService = fileService; @@ -53,6 +58,7 @@ public AttachmentUnitService(SlideRepository slideRepository, SlideSplitterServi this.slideRepository = slideRepository; this.pyrisWebhookService = pyrisWebhookService; this.irisSettingsRepository = irisSettingsRepository; + this.competencyProgressService = competencyProgressService; } /** @@ -98,9 +104,12 @@ public AttachmentUnit createAttachmentUnit(AttachmentUnit attachmentUnit, Attach */ public AttachmentUnit updateAttachmentUnit(AttachmentUnit existingAttachmentUnit, AttachmentUnit updateUnit, Attachment updateAttachment, MultipartFile updateFile, boolean keepFilename) { + Set existingCompetencies = existingAttachmentUnit.getCompetencies(); + existingAttachmentUnit.setDescription(updateUnit.getDescription()); existingAttachmentUnit.setName(updateUnit.getName()); existingAttachmentUnit.setReleaseDate(updateUnit.getReleaseDate()); + existingAttachmentUnit.setCompetencies(updateUnit.getCompetencies()); AttachmentUnit savedAttachmentUnit = attachmentUnitRepository.saveAndFlush(existingAttachmentUnit); @@ -134,6 +143,11 @@ public AttachmentUnit updateAttachmentUnit(AttachmentUnit existingAttachmentUnit pyrisWebhookService.get().autoUpdateAttachmentUnitsInPyris(savedAttachmentUnit.getLecture().getCourse().getId(), List.of(savedAttachmentUnit)); } } + + // Set the original competencies back to the attachment unit so that the competencyProgressService can determine which competencies changed + existingAttachmentUnit.setCompetencies(existingCompetencies); + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(existingAttachmentUnit, Optional.of(updateUnit)); + return savedAttachmentUnit; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java index 8ebfb834faa4..8015980acfe5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java @@ -520,7 +520,7 @@ private void deleteNotificationsOfCourse(Course course) { private void deleteLecturesOfCourse(Course course) { for (Lecture lecture : course.getLectures()) { - lectureService.delete(lecture); + lectureService.delete(lecture, false); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java b/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java index 635e8938c36f..5c873d1149fa 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ExerciseDeletionService.java @@ -16,6 +16,7 @@ import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.exam.StudentExam; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; @@ -28,6 +29,7 @@ import de.tum.in.www1.artemis.repository.TutorParticipationRepository; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseService; import de.tum.in.www1.artemis.service.quiz.QuizExerciseService; @@ -70,11 +72,13 @@ public class ExerciseDeletionService { private final ChannelService channelService; + private final CompetencyProgressService competencyProgressService; + public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUnitRepository exerciseUnitRepository, ParticipationService participationService, ProgrammingExerciseService programmingExerciseService, ModelingExerciseService modelingExerciseService, QuizExerciseService quizExerciseService, TutorParticipationRepository tutorParticipationRepository, ExampleSubmissionService exampleSubmissionService, StudentExamRepository studentExamRepository, LectureUnitService lectureUnitService, PlagiarismResultRepository plagiarismResultRepository, TextExerciseService textExerciseService, - ChannelRepository channelRepository, ChannelService channelService) { + ChannelRepository channelRepository, ChannelService channelService, CompetencyProgressService competencyProgressService) { this.exerciseRepository = exerciseRepository; this.participationService = participationService; this.programmingExerciseService = programmingExerciseService; @@ -89,6 +93,7 @@ public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUn this.textExerciseService = textExerciseService; this.channelRepository = channelRepository; this.channelService = channelService; + this.competencyProgressService = competencyProgressService; } /** @@ -134,6 +139,7 @@ public void cleanup(Long exerciseId, boolean deleteRepositories) { */ public void delete(long exerciseId, boolean deleteStudentReposBuildPlans, boolean deleteBaseReposBuildPlans) { var exercise = exerciseRepository.findWithCompetenciesByIdElseThrow(exerciseId); + Set competencies = exercise.getCompetencies(); log.info("Request to delete {} with id {}", exercise.getClass().getSimpleName(), exerciseId); long start = System.nanoTime(); @@ -163,7 +169,7 @@ public void delete(long exerciseId, boolean deleteStudentReposBuildPlans, boolea plagiarismResultRepository.deletePlagiarismResultsByExerciseId(exerciseId); // delete all participations belonging to this exercise, this will also delete submissions, results, feedback, complaints, etc. - participationService.deleteAllByExerciseId(exercise.getId(), deleteStudentReposBuildPlans, deleteStudentReposBuildPlans); + participationService.deleteAllByExercise(exercise, deleteStudentReposBuildPlans, deleteStudentReposBuildPlans, false); // clean up the many-to-many relationship to avoid problems when deleting the entities but not the relationship table exercise = exerciseRepository.findByIdWithEagerExampleSubmissionsElseThrow(exerciseId); @@ -193,6 +199,8 @@ public void delete(long exerciseId, boolean deleteStudentReposBuildPlans, boolea exercise = exerciseRepository.findByIdWithStudentParticipationsElseThrow(exerciseId); exerciseRepository.delete(exercise); } + + competencies.forEach(competencyProgressService::updateProgressByCompetencyAsync); } /** @@ -221,6 +229,6 @@ public void deletePlagiarismResultsAndParticipations(Exercise exercise) { plagiarismResultRepository.deletePlagiarismResultsByExerciseId(exercise.getId()); // delete all participations belonging to this exercise, this will also delete submissions, results, feedback, complaints, etc. - participationService.deleteAllByExerciseId(exercise.getId(), true, true); + participationService.deleteAllByExercise(exercise, true, true, true); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/ExerciseImportService.java index e6d1059cd455..6ac00b2c0157 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ExerciseImportService.java @@ -62,6 +62,7 @@ protected void copyExerciseBasis(final Exercise newExercise, final Exercise impo newExercise.setDifficulty(importedExercise.getDifficulty()); newExercise.setGradingInstructions(importedExercise.getGradingInstructions()); newExercise.setGradingCriteria(importedExercise.copyGradingCriteria(gradingInstructionCopyTracker)); + newExercise.setCompetencies(importedExercise.getCompetencies()); if (importedExercise.getPlagiarismDetectionConfig() != null) { newExercise.setPlagiarismDetectionConfig(new PlagiarismDetectionConfig(importedExercise.getPlagiarismDetectionConfig())); diff --git a/src/main/java/de/tum/in/www1/artemis/service/FileUploadExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/FileUploadExerciseImportService.java index aae697d853ab..601e8b732bef 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FileUploadExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FileUploadExerciseImportService.java @@ -17,6 +17,7 @@ import de.tum.in.www1.artemis.repository.FileUploadExerciseRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; @Profile(PROFILE_CORE) @@ -29,11 +30,15 @@ public class FileUploadExerciseImportService extends ExerciseImportService { private final ChannelService channelService; + private final CompetencyProgressService competencyProgressService; + public FileUploadExerciseImportService(ExampleSubmissionRepository exampleSubmissionRepository, SubmissionRepository submissionRepository, ResultRepository resultRepository, - FileUploadExerciseRepository fileUploadExerciseRepository, ChannelService channelService, FeedbackService feedbackService) { + FileUploadExerciseRepository fileUploadExerciseRepository, ChannelService channelService, FeedbackService feedbackService, + CompetencyProgressService competencyProgressService) { super(exampleSubmissionRepository, submissionRepository, resultRepository, feedbackService); this.fileUploadExerciseRepository = fileUploadExerciseRepository; this.channelService = channelService; + this.competencyProgressService = competencyProgressService; } /** @@ -54,6 +59,8 @@ public FileUploadExercise importFileUploadExercise(final FileUploadExercise temp channelService.createExerciseChannel(newFileUploadExercise, Optional.ofNullable(importedExercise.getChannelName())); + competencyProgressService.updateProgressByLearningObjectAsync(newFileUploadExercise); + return newFileUploadExercise; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/LearningObjectService.java b/src/main/java/de/tum/in/www1/artemis/service/LearningObjectService.java index 50c9ef6610b2..17e0c0167e27 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LearningObjectService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LearningObjectService.java @@ -3,10 +3,8 @@ import static de.tum.in.www1.artemis.config.Constants.MIN_SCORE_GREEN; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; -import java.util.Comparator; import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; import jakarta.validation.constraints.NotNull; @@ -16,7 +14,6 @@ import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.LearningObject; import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnitCompletion; @@ -91,19 +88,6 @@ private boolean isCompletedByUser(Exercise exercise, User user) { } } - /** - * Get the completed learning objects for the given user and competencies. - * - * @param user the user for which to get the completed learning objects - * @param competencies the competencies for which to get the completed learning objects - * @return the completed learning objects for the given user and competencies - */ - public Stream getCompletedLearningObjectsForUserAndCompetencies(User user, Set competencies) { - return Stream.concat(competencies.stream().map(CourseCompetency::getLectureUnits), competencies.stream().map(CourseCompetency::getExercises)).flatMap(Set::stream) - .filter(learningObject -> learningObject.getCompletionDate(user).isPresent()) - .sorted(Comparator.comparing(learningObject -> learningObject.getCompletionDate(user).orElseThrow())).map(LearningObject.class::cast); - } - /** * Get learning object by id and type. * diff --git a/src/main/java/de/tum/in/www1/artemis/service/LectureService.java b/src/main/java/de/tum/in/www1/artemis/service/LectureService.java index 3a563bbd7b04..d722fd2673da 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LectureService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LectureService.java @@ -18,10 +18,12 @@ import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; +import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; import de.tum.in.www1.artemis.repository.LectureRepository; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.connectors.pyris.PyrisWebhookService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; @@ -42,13 +44,16 @@ public class LectureService { private final Optional pyrisWebhookService; + private final CompetencyProgressService competencyProgressService; + public LectureService(LectureRepository lectureRepository, AuthorizationCheckService authCheckService, ChannelRepository channelRepository, ChannelService channelService, - Optional pyrisWebhookService) { + Optional pyrisWebhookService, CompetencyProgressService competencyProgressService) { this.lectureRepository = lectureRepository; this.authCheckService = authCheckService; this.channelRepository = channelRepository; this.channelService = channelService; this.pyrisWebhookService = pyrisWebhookService; + this.competencyProgressService = competencyProgressService; } /** @@ -136,9 +141,10 @@ public SearchResultPageDTO getAllOnPageWithSize(final SearchTermPageabl /** * Deletes the given lecture (with its lecture units). * - * @param lecture the lecture to be deleted + * @param lecture the lecture to be deleted + * @param updateCompetencyProgress whether the competency progress should be updated */ - public void delete(Lecture lecture) { + public void delete(Lecture lecture, boolean updateCompetencyProgress) { if (pyrisWebhookService.isPresent()) { Lecture lectureWithAttachmentUnits = lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lecture.getId()); List attachmentUnitList = lectureWithAttachmentUnits.getLectureUnits().stream().filter(lectureUnit -> lectureUnit instanceof AttachmentUnit) @@ -147,6 +153,12 @@ public void delete(Lecture lecture) { pyrisWebhookService.get().deleteLectureFromPyrisDB(attachmentUnitList); } } + + if (updateCompetencyProgress) { + lecture.getLectureUnits().stream().filter(lectureUnit -> !(lectureUnit instanceof ExerciseUnit)) + .forEach(lectureUnit -> competencyProgressService.updateProgressForUpdatedLearningObjectAsync(lectureUnit, Optional.empty())); + } + Channel lectureChannel = channelRepository.findChannelByLectureId(lecture.getId()); channelService.deleteChannel(lectureChannel); lectureRepository.deleteById(lecture.getId()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java index c38316091cc4..e09ea847078a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/LectureUnitService.java @@ -36,6 +36,7 @@ import de.tum.in.www1.artemis.repository.LectureUnitCompletionRepository; import de.tum.in.www1.artemis.repository.LectureUnitRepository; import de.tum.in.www1.artemis.repository.SlideRepository; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.connectors.pyris.PyrisWebhookService; @Profile(PROFILE_CORE) @@ -56,11 +57,13 @@ public class LectureUnitService { private final Optional pyrisWebhookService; + private final CompetencyProgressService competencyProgressService; + private final CourseCompetencyRepository courseCompetencyRepository; public LectureUnitService(LectureUnitRepository lectureUnitRepository, LectureRepository lectureRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, FileService fileService, SlideRepository slideRepository, ExerciseRepository exerciseRepository, Optional pyrisWebhookService, - CourseCompetencyRepository courseCompetencyRepository) { + CompetencyProgressService competencyProgressService, CourseCompetencyRepository courseCompetencyRepository) { this.lectureUnitRepository = lectureUnitRepository; this.lectureRepository = lectureRepository; this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; @@ -69,6 +72,7 @@ public LectureUnitService(LectureUnitRepository lectureUnitRepository, LectureRe this.exerciseRepository = exerciseRepository; this.pyrisWebhookService = pyrisWebhookService; this.courseCompetencyRepository = courseCompetencyRepository; + this.competencyProgressService = competencyProgressService; } /** @@ -176,6 +180,11 @@ public void removeLectureUnit(@NotNull LectureUnit lectureUnit) { // Creating a new list of lecture units without the one we want to remove lecture.getLectureUnits().removeIf(unit -> unit == null || unit.getId().equals(lectureUnitToDelete.getId())); lectureRepository.save(lecture); + + if (!(lectureUnitToDelete instanceof ExerciseUnit)) { + // update associated competency progress objects + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(lectureUnitToDelete, Optional.empty()); + } } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/ModelingExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/ModelingExerciseImportService.java index 0e08546135a0..87de18c16f4b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ModelingExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ModelingExerciseImportService.java @@ -26,6 +26,7 @@ import de.tum.in.www1.artemis.repository.ModelingExerciseRepository; import de.tum.in.www1.artemis.repository.ResultRepository; import de.tum.in.www1.artemis.repository.SubmissionRepository; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; @Profile(PROFILE_CORE) @@ -38,11 +39,15 @@ public class ModelingExerciseImportService extends ExerciseImportService { private final ChannelService channelService; + private final CompetencyProgressService competencyProgressService; + public ModelingExerciseImportService(ModelingExerciseRepository modelingExerciseRepository, ExampleSubmissionRepository exampleSubmissionRepository, - SubmissionRepository submissionRepository, ResultRepository resultRepository, ChannelService channelService, FeedbackService feedbackService) { + SubmissionRepository submissionRepository, ResultRepository resultRepository, ChannelService channelService, FeedbackService feedbackService, + CompetencyProgressService competencyProgressService) { super(exampleSubmissionRepository, submissionRepository, resultRepository, feedbackService); this.modelingExerciseRepository = modelingExerciseRepository; this.channelService = channelService; + this.competencyProgressService = competencyProgressService; } /** @@ -65,6 +70,9 @@ public ModelingExercise importModelingExercise(ModelingExercise templateExercise channelService.createExerciseChannel(newModelingExercise, Optional.ofNullable(importedExercise.getChannelName())); newModelingExercise.setExampleSubmissions(copyExampleSubmission(templateExercise, newExercise, gradingInstructionCopyTracker)); + + competencyProgressService.updateProgressByLearningObjectAsync(newModelingExercise); + return newModelingExercise; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipantScoreService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipantScoreService.java index e02dff650692..a1c12af059f5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipantScoreService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipantScoreService.java @@ -216,4 +216,19 @@ public double getAverageOfAverageScores(Set exercises) { return participantScoreRepository.findAverageScoreForExercises(exercises).stream().mapToDouble(exerciseInfo -> (double) exerciseInfo.get("averageScore")).average() .orElse(0.0); } + + /** + * Get all users that participated in the given exercise. + * + * @param exercise the exercise for which to get all users that participated + * @return set of users that participated in the exercise + */ + public Set getAllParticipatedUsersInExercise(Exercise exercise) { + if (exercise.isTeamMode()) { + return teamScoreRepository.findAllUsersWithScoresByExercise(exercise); + } + else { + return studentScoreRepository.findAllUsersWithScoresByExercise(exercise); + } + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java index 601c49c3ef0a..f1cb7d607d93 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/ParticipationService.java @@ -47,6 +47,7 @@ import de.tum.in.www1.artemis.repository.TeamRepository; import de.tum.in.www1.artemis.repository.TeamScoreRepository; import de.tum.in.www1.artemis.repository.hestia.CoverageReportRepository; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService; import de.tum.in.www1.artemis.service.connectors.localci.SharedQueueManagementService; @@ -100,13 +101,15 @@ public class ParticipationService { private final ProfileService profileService; + private final CompetencyProgressService competencyProgressService; + public ParticipationService(GitService gitService, Optional continuousIntegrationService, Optional versionControlService, BuildLogEntryService buildLogEntryService, ParticipationRepository participationRepository, StudentParticipationRepository studentParticipationRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProgrammingExerciseRepository programmingExerciseRepository, SubmissionRepository submissionRepository, TeamRepository teamRepository, UriService uriService, ResultService resultService, CoverageReportRepository coverageReportRepository, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, ParticipantScoreRepository participantScoreRepository, StudentScoreRepository studentScoreRepository, TeamScoreRepository teamScoreRepository, - Optional localCISharedBuildJobQueueService, ProfileService profileService) { + Optional localCISharedBuildJobQueueService, ProfileService profileService, CompetencyProgressService competencyProgressService) { this.gitService = gitService; this.continuousIntegrationService = continuousIntegrationService; this.versionControlService = versionControlService; @@ -126,6 +129,7 @@ public ParticipationService(GitService gitService, Optional exercise.getCourseViaExerciseGroupOrCourseMember(); - case LectureUnit lectureUnit -> lectureUnit.getLecture().getCourse(); - default -> throw new IllegalArgumentException("Learning object must be either LectureUnit or Exercise"); - }; - updateProgressByLearningObject(learningObject, userRepository.getStudents(course)); + SecurityUtils.setAuthorizationObject(); // Required for async + Set competencyIds = courseCompetencyRepository.findAllIdsByLearningObject(learningObject); + + for (long competencyId : competencyIds) { + Set users = competencyProgressRepository.findAllByCompetencyId(competencyId).stream().map(CompetencyProgress::getUser).collect(Collectors.toSet()); + log.debug("Updating competency progress for {} users.", users.size()); + + users.forEach(user -> updateCompetencyProgress(competencyId, user)); + } } /** @@ -125,36 +120,85 @@ public void updateProgressByLearningObjectAsync(LearningObject learningObject) { */ @Async public void updateProgressByCompetencyAsync(CourseCompetency competency) { - SecurityUtils.setAuthorizationObject(); // required for async - competencyProgressRepository.findAllByCompetencyId(competency.getId()).stream().map(CompetencyProgress::getUser) - .forEach(user -> updateCompetencyProgress(competency.getId(), user)); + SecurityUtils.setAuthorizationObject(); // Required for async + List existingProgress = competencyProgressRepository.findAllByCompetencyId(competency.getId()); + log.debug("Updating competency progress for {} users.", existingProgress.size()); + existingProgress.stream().map(CompetencyProgress::getUser).forEach(user -> updateCompetencyProgress(competency.getId(), user)); } /** - * Update the progress for all competencies linked to the given learning object + * Asynchronously update the progress of all users in the course for a specific competency * - * @param learningObject The learning object for which to fetch the competencies - * @param users A list of users for which to update the progress + * @param competency The competency for which to update all existing student progress */ - public void updateProgressByLearningObject(LearningObject learningObject, @NotNull Set users) { + @Async + public void updateProgressByCompetencyAndUsersInCourseAsync(CourseCompetency competency) { + SecurityUtils.setAuthorizationObject(); // Required for async + Set users = userRepository.getUsersInCourse(competency.getCourse()); log.debug("Updating competency progress for {} users.", users.size()); - try { - Set competencies = switch (learningObject) { - case Exercise exercise -> exerciseRepository.findWithCompetenciesById(exercise.getId()).map(Exercise::getCompetencies).orElse(null); - case LectureUnit lectureUnit -> lectureUnitRepository.findWithCompetenciesById(lectureUnit.getId()).map(LectureUnit::getCompetencies).orElse(null); - default -> throw new IllegalArgumentException("Learning object must be either LectureUnit or Exercise"); - }; + users.forEach(user -> updateCompetencyProgress(competency.getId(), user)); + } - if (competencies == null) { - // Competencies couldn't be loaded, the exercise/lecture unit might have already been deleted - log.debug("Competencies could not be fetched, skipping."); - return; - } + /** + * Asynchronously update the existing progress for all changed competencies linked to the given learning object + * If new competencies are added, the progress is updated for all users in the course, otherwise only the existing progresses are updated. + * + * @param originalLearningObject The original learning object before the update + * @param updatedLearningObject The updated learning object after the update (empty if the learning object was deleted) + */ + @Async + public void updateProgressForUpdatedLearningObjectAsync(LearningObject originalLearningObject, Optional updatedLearningObject) { + SecurityUtils.setAuthorizationObject(); // Required for async + + Set originalCompetencyIds = originalLearningObject.getCompetencies().stream().map(CourseCompetency::getId).collect(Collectors.toSet()); + Set updatedCompetencies = updatedLearningObject.map(LearningObject::getCompetencies).orElse(Set.of()); + Set updatedCompetencyIds = updatedCompetencies.stream().map(CourseCompetency::getId).collect(Collectors.toSet()); + + Set removedCompetencyIds = originalCompetencyIds.stream().filter(id -> !updatedCompetencyIds.contains(id)).collect(Collectors.toSet()); + Set addedCompetencyIds = updatedCompetencyIds.stream().filter(id -> !originalCompetencyIds.contains(id)).collect(Collectors.toSet()); + + updateProgressByCompetencyIds(removedCompetencyIds); + if (!addedCompetencyIds.isEmpty()) { + updateProgressByCompetencyIdsAndLearningObject(addedCompetencyIds, originalLearningObject); + } + } - users.forEach(user -> competencies.forEach(competency -> updateCompetencyProgress(competency.getId(), user))); + private void updateProgressByCompetencyIds(Set competencyIds) { + for (long competencyId : competencyIds) { + List existingProgress = competencyProgressRepository.findAllByCompetencyId(competencyId); + log.debug("Updating competency progress for {} users.", existingProgress.size()); + existingProgress.stream().map(CompetencyProgress::getUser).forEach(user -> updateCompetencyProgress(competencyId, user)); } - catch (Exception e) { - log.error("Exception while updating progress for competency", e); + } + + private void updateProgressByCompetencyIdsAndLearningObject(Set competencyIds, LearningObject learningObject) { + for (long competencyId : competencyIds) { + Set existingCompetencyUsers = competencyProgressRepository.findAllByCompetencyId(competencyId).stream().map(CompetencyProgress::getUser) + .collect(Collectors.toSet()); + Set existingLearningObjectUsers = switch (learningObject) { + case Exercise exercise -> participantScoreService.getAllParticipatedUsersInExercise(exercise); + case LectureUnit lectureUnit -> lectureUnitCompletionRepository.findCompletedUsersForLectureUnit(lectureUnit); + default -> throw new IllegalStateException("Unexpected value: " + learningObject); + }; + existingCompetencyUsers.addAll(existingLearningObjectUsers); + log.debug("Updating competency progress for {} users.", existingCompetencyUsers.size()); + existingCompetencyUsers.forEach(user -> updateCompetencyProgress(competencyId, user)); + } + } + + /** + * Update the progress for all competencies linked to the given learning object synchronously + * + * @param learningObject The learning object for which to fetch the competencies + * @param users The users for which to update the progress + */ + public void updateProgressByLearningObjectSync(LearningObject learningObject, Set users) { + Set competencyIds = courseCompetencyRepository.findAllIdsByLearningObject(learningObject); + + for (long competencyId : competencyIds) { + log.debug("Updating competency progress synchronously for {} users.", users.size()); + + users.forEach(user -> updateCompetencyProgress(competencyId, user)); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyRelationService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyRelationService.java index 0d17fad767ab..bf211246c8c6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyRelationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyRelationService.java @@ -12,7 +12,6 @@ import de.tum.in.www1.artemis.domain.competency.CourseCompetency; import de.tum.in.www1.artemis.domain.competency.RelationType; import de.tum.in.www1.artemis.repository.CompetencyRelationRepository; -import de.tum.in.www1.artemis.repository.CompetencyRepository; import de.tum.in.www1.artemis.repository.CourseCompetencyRepository; /** @@ -27,15 +26,12 @@ public class CompetencyRelationService { private final CompetencyService competencyService; - private final CompetencyRepository competencyRepository; - private final CourseCompetencyRepository courseCompetencyRepository; - public CompetencyRelationService(CompetencyRelationRepository competencyRelationRepository, CompetencyService competencyService, CompetencyRepository competencyRepository, + public CompetencyRelationService(CompetencyRelationRepository competencyRelationRepository, CompetencyService competencyService, CourseCompetencyRepository courseCompetencyRepository) { this.competencyRelationRepository = competencyRelationRepository; this.competencyService = competencyService; - this.competencyRepository = competencyRepository; this.courseCompetencyRepository = courseCompetencyRepository; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java index bf434a009e66..fad3f2fa0c6d 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/competency/CompetencyService.java @@ -228,7 +228,7 @@ public Competency createCompetency(Competency competency, Course course) { var persistedCompetency = competencyRepository.save(competencyToCreate); - lectureUnitService.linkLectureUnitsToCompetency(persistedCompetency, competency.getLectureUnits(), Set.of()); + updateLectureUnits(competency, persistedCompetency); if (course.getLearningPathsEnabled()) { learningPathService.linkCompetencyToLearningPathsOfCourse(persistedCompetency, course.getId()); @@ -252,7 +252,8 @@ public List createCompetencies(List competencies, Course createdCompetency.setCourse(course); createdCompetency = competencyRepository.save(createdCompetency); - lectureUnitService.linkLectureUnitsToCompetency(createdCompetency, competency.getLectureUnits(), Set.of()); + updateLectureUnits(competency, createdCompetency); + createdCompetencies.add(createdCompetency); } @@ -263,6 +264,13 @@ public List createCompetencies(List competencies, Course return createdCompetencies; } + private void updateLectureUnits(Competency competency, Competency createdCompetency) { + if (!competency.getLectureUnits().isEmpty()) { + lectureUnitService.linkLectureUnitsToCompetency(createdCompetency, competency.getLectureUnits(), Set.of()); + competencyProgressService.updateProgressByCompetencyAndUsersInCourseAsync(createdCompetency); + } + } + /** * Updates a competency with the values of another one. Updates progress if necessary. * @@ -282,7 +290,7 @@ public Competency updateCompetency(Competency competencyToUpdate, Competency com // update competency progress if necessary if (competency.getLectureUnits().size() != competencyToUpdate.getLectureUnits().size() || !competencyToUpdate.getLectureUnits().containsAll(competency.getLectureUnits())) { log.debug("Linked lecture units changed, updating student progress for competency..."); - competencyProgressService.updateProgressByCompetencyAsync(persistedCompetency); + competencyProgressService.updateProgressByCompetencyAndUsersInCourseAsync(persistedCompetency); } return persistedCompetency; diff --git a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java index a5d48f2598e6..462cdc51827f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/learningpath/LearningPathRecommendationService.java @@ -134,7 +134,7 @@ public RecommendationState getRecommendedOrderOfAllCompetencies(LearningPath lea } /** - * Gets the next due learning object of a learning path + * Gets the first learning object of a learning path * * @param user the user that should be analyzed * @param recommendationState the current state of the learning path recommendation @@ -173,18 +173,6 @@ public LearningObject getLastLearningObject(User user, RecommendationState recom return learningObject; } - /** - * Gets the uncompleted learning objects of a learning path - * - * @param learningPath the learning path that should be analyzed - * @return the uncompleted learning objects of the learning path - */ - public Stream getUncompletedLearningObjects(LearningPath learningPath) { - var recommendationState = getRecommendedOrderOfNotMasteredCompetencies(learningPath); - return recommendationState.recommendedOrderOfCompetencies.stream().map(recommendationState.competencyIdMap::get) - .flatMap(competency -> getRecommendedOrderOfLearningObjects(learningPath.getUser(), competency, recommendationState).stream()); - } - /** * Generates the initial state of the recommendation containing all necessary information for the prediction. * diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java index b51e5a47b975..57227a20ddca 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java @@ -73,6 +73,7 @@ import de.tum.in.www1.artemis.service.ParticipationService; import de.tum.in.www1.artemis.service.ProfileService; import de.tum.in.www1.artemis.service.SubmissionPolicyService; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.connectors.BuildScriptGenerationService; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.connectors.aeolus.AeolusTemplateService; @@ -86,7 +87,6 @@ import de.tum.in.www1.artemis.service.messaging.InstanceMessageSendService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationScheduleService; -import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; import de.tum.in.www1.artemis.service.util.structureoraclegenerator.OracleGenerator; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; @@ -134,8 +134,6 @@ public class ProgrammingExerciseService { private final UserRepository userRepository; - private final GroupNotificationService groupNotificationService; - private final GroupNotificationScheduleService groupNotificationScheduleService; private final InstanceMessageSendService instanceMessageSendService; @@ -182,11 +180,13 @@ public class ProgrammingExerciseService { private final ExerciseService exerciseService; + private final CompetencyProgressService competencyProgressService; + public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerciseRepository, GitService gitService, Optional versionControlService, Optional continuousIntegrationService, Optional continuousIntegrationTriggerService, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, ParticipationService participationService, - ParticipationRepository participationRepository, ResultRepository resultRepository, UserRepository userRepository, GroupNotificationService groupNotificationService, + ParticipationRepository participationRepository, ResultRepository resultRepository, UserRepository userRepository, GroupNotificationScheduleService groupNotificationScheduleService, InstanceMessageSendService instanceMessageSendService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, ProgrammingExerciseTaskRepository programmingExerciseTaskRepository, ProgrammingExerciseSolutionEntryRepository programmingExerciseSolutionEntryRepository, ProgrammingExerciseTaskService programmingExerciseTaskService, @@ -195,7 +195,8 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc SubmissionPolicyService submissionPolicyService, Optional programmingLanguageFeatureService, ChannelService channelService, ProgrammingSubmissionService programmingSubmissionService, Optional irisSettingsService, Optional aeolusTemplateService, Optional buildScriptGenerationService, - ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProfileService profileService, ExerciseService exerciseService) { + ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProfileService profileService, ExerciseService exerciseService, + CompetencyProgressService competencyProgressService) { this.programmingExerciseRepository = programmingExerciseRepository; this.gitService = gitService; this.versionControlService = versionControlService; @@ -207,7 +208,6 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc this.participationService = participationService; this.resultRepository = resultRepository; this.userRepository = userRepository; - this.groupNotificationService = groupNotificationService; this.groupNotificationScheduleService = groupNotificationScheduleService; this.instanceMessageSendService = instanceMessageSendService; this.auxiliaryRepositoryRepository = auxiliaryRepositoryRepository; @@ -228,6 +228,7 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository; this.profileService = profileService; this.exerciseService = exerciseService; + this.competencyProgressService = competencyProgressService; } /** @@ -326,6 +327,8 @@ public ProgrammingExercise createProgrammingExercise(ProgrammingExercise program scheduleOperations(savedProgrammingExercise.getId()); // Step 12c: Check notifications for new exercise groupNotificationScheduleService.checkNotificationsForNewExerciseAsync(savedProgrammingExercise); + // Step 12d: Update student competency progress + competencyProgressService.updateProgressByLearningObjectAsync(savedProgrammingExercise); return programmingExerciseRepository.saveForCreation(savedProgrammingExercise); } @@ -565,6 +568,8 @@ public ProgrammingExercise updateProgrammingExercise(ProgrammingExercise program exerciseService.notifyAboutExerciseChanges(programmingExerciseBeforeUpdate, updatedProgrammingExercise, notificationText); + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(programmingExerciseBeforeUpdate, Optional.of(updatedProgrammingExercise)); + return savedProgrammingExercise; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseImportService.java index 3c328b89e72d..df3cc8931b72 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/quiz/QuizExerciseImportService.java @@ -36,6 +36,7 @@ import de.tum.in.www1.artemis.service.FeedbackService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; @Profile(PROFILE_CORE) @@ -50,12 +51,16 @@ public class QuizExerciseImportService extends ExerciseImportService { private final ChannelService channelService; + private final CompetencyProgressService competencyProgressService; + public QuizExerciseImportService(QuizExerciseService quizExerciseService, FileService fileService, ExampleSubmissionRepository exampleSubmissionRepository, - SubmissionRepository submissionRepository, ResultRepository resultRepository, ChannelService channelService, FeedbackService feedbackService) { + SubmissionRepository submissionRepository, ResultRepository resultRepository, ChannelService channelService, FeedbackService feedbackService, + CompetencyProgressService competencyProgressService) { super(exampleSubmissionRepository, submissionRepository, resultRepository, feedbackService); this.quizExerciseService = quizExerciseService; this.fileService = fileService; this.channelService = channelService; + this.competencyProgressService = competencyProgressService; } /** @@ -78,6 +83,9 @@ public QuizExercise importQuizExercise(final QuizExercise templateExercise, Quiz QuizExercise newQuizExercise = quizExerciseService.save(newExercise); channelService.createExerciseChannel(newQuizExercise, Optional.ofNullable(importedExercise.getChannelName())); + + competencyProgressService.updateProgressByLearningObjectAsync(newQuizExercise); + return newQuizExercise; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ParticipantScoreScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ParticipantScoreScheduleService.java index a87b1924beac..3ce5c86aba16 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/ParticipantScoreScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/ParticipantScoreScheduleService.java @@ -335,7 +335,7 @@ private void executeTask(Long exerciseId, Long participantId, Instant resultLast if (scoreParticipant instanceof Team team && !Hibernate.isInitialized(team.getStudents())) { scoreParticipant = teamRepository.findWithStudentsByIdElseThrow(team.getId()); } - competencyProgressService.updateProgressByLearningObject(score.getExercise(), scoreParticipant.getParticipants()); + competencyProgressService.updateProgressByLearningObjectSync(score.getExercise(), scoreParticipant.getParticipants()); } catch (Exception e) { log.error("Exception while processing participant score for exercise {} and participant {} for participant scores:", exerciseId, participantId, e); diff --git a/src/main/java/de/tum/in/www1/artemis/service/team/strategies/PurgeExistingStrategy.java b/src/main/java/de/tum/in/www1/artemis/service/team/strategies/PurgeExistingStrategy.java index 8843bbb91ad1..99323d158f67 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/team/strategies/PurgeExistingStrategy.java +++ b/src/main/java/de/tum/in/www1/artemis/service/team/strategies/PurgeExistingStrategy.java @@ -57,7 +57,7 @@ public void importTeams(Exercise exercise, List teams) { */ private void deleteExistingTeamsAndAddNewTeams(Exercise exercise, List teams) { // Delete participations of existing teams in destination exercise (must happen before deleting teams themselves) - participationService.deleteAllByExerciseId(exercise.getId(), false, false); + participationService.deleteAllByExercise(exercise, false, false, true); // Purge existing teams in destination exercise List destinationTeams = teamRepository.findAllByExerciseId(exercise.getId()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FileUploadExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FileUploadExerciseResource.java index f441164f9093..986f0ee9015d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FileUploadExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FileUploadExerciseResource.java @@ -48,6 +48,7 @@ import de.tum.in.www1.artemis.service.ExerciseService; import de.tum.in.www1.artemis.service.FileUploadExerciseImportService; import de.tum.in.www1.artemis.service.FileUploadExerciseService; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.export.FileUploadSubmissionExportService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; @@ -106,12 +107,14 @@ public class FileUploadExerciseResource { private final ChannelRepository channelRepository; + private final CompetencyProgressService competencyProgressService; + public FileUploadExerciseResource(FileUploadExerciseRepository fileUploadExerciseRepository, UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService, ExerciseService exerciseService, ExerciseDeletionService exerciseDeletionService, FileUploadSubmissionExportService fileUploadSubmissionExportService, GradingCriterionRepository gradingCriterionRepository, CourseRepository courseRepository, ParticipationRepository participationRepository, GroupNotificationScheduleService groupNotificationScheduleService, FileUploadExerciseImportService fileUploadExerciseImportService, FileUploadExerciseService fileUploadExerciseService, ChannelService channelService, - ChannelRepository channelRepository) { + ChannelRepository channelRepository, CompetencyProgressService competencyProgressService) { this.fileUploadExerciseRepository = fileUploadExerciseRepository; this.userRepository = userRepository; this.courseService = courseService; @@ -127,6 +130,7 @@ public FileUploadExerciseResource(FileUploadExerciseRepository fileUploadExercis this.fileUploadExerciseService = fileUploadExerciseService; this.channelService = channelService; this.channelRepository = channelRepository; + this.competencyProgressService = competencyProgressService; } /** @@ -156,6 +160,7 @@ public ResponseEntity createFileUploadExercise(@RequestBody channelService.createExerciseChannel(result, Optional.ofNullable(fileUploadExercise.getChannelName())); groupNotificationScheduleService.checkNotificationsForNewExerciseAsync(fileUploadExercise); + competencyProgressService.updateProgressByLearningObjectAsync(result); return ResponseEntity.created(new URI("/api/file-upload-exercises/" + result.getId())).body(result); } @@ -181,7 +186,7 @@ public ResponseEntity importFileUploadExercise(@PathVariable if (sourceId <= 0 || (importedFileUploadExercise.getCourseViaExerciseGroupOrCourseMember() == null && importedFileUploadExercise.getExerciseGroup() == null)) { throw new BadRequestAlertException("Either the courseId or exerciseGroupId must be set for an import", ENTITY_NAME, "noCourseIdOrExerciseGroupId"); } - importedFileUploadExercise.checkCourseAndExerciseGroupExclusivity("File Upload Exercise"); + importedFileUploadExercise.checkCourseAndExerciseGroupExclusivity(ENTITY_NAME); final var user = userRepository.getUserWithGroupsAndAuthorities(); final var originalFileUploadExercise = fileUploadExerciseRepository.findByIdElseThrow(sourceId); @@ -269,7 +274,7 @@ public ResponseEntity updateFileUploadExercise(@RequestBody // Check that the user is authorized to update the exercise User user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, user); - final var fileUploadExerciseBeforeUpdate = fileUploadExerciseRepository.findByIdElseThrow(fileUploadExercise.getId()); + final var fileUploadExerciseBeforeUpdate = fileUploadExerciseRepository.findWithEagerCompetenciesByIdElseThrow(fileUploadExercise.getId()); // Forbid conversion between normal course exercise and exam exercise exerciseService.checkForConversionBetweenExamAndCourseExercise(fileUploadExercise, fileUploadExerciseBeforeUpdate, ENTITY_NAME); @@ -282,6 +287,7 @@ public ResponseEntity updateFileUploadExercise(@RequestBody participationRepository.removeIndividualDueDatesIfBeforeDueDate(updatedExercise, fileUploadExerciseBeforeUpdate.getDueDate()); exerciseService.notifyAboutExerciseChanges(fileUploadExerciseBeforeUpdate, updatedExercise, notificationText); + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(fileUploadExerciseBeforeUpdate, Optional.of(fileUploadExercise)); return ResponseEntity.ok(updatedExercise); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java index 7c3b39e534e1..9aa73d603557 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/LectureResource.java @@ -395,7 +395,7 @@ else if (lectureUnit instanceof AttachmentUnit) { @DeleteMapping("lectures/{lectureId}") @EnforceAtLeastInstructor public ResponseEntity deleteLecture(@PathVariable Long lectureId) { - Lecture lecture = lectureRepository.findByIdElseThrow(lectureId); + Lecture lecture = lectureRepository.findByIdWithLectureUnitsAndCompetenciesElseThrow(lectureId); Course course = lecture.getCourse(); if (course == null) { @@ -404,7 +404,7 @@ public ResponseEntity deleteLecture(@PathVariable Long lectureId) { authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); log.debug("REST request to delete Lecture : {}", lectureId); - lectureService.delete(lecture); + lectureService.delete(lecture, true); return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, lectureId.toString())).build(); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java index 5d0bcf9f9b4c..739cd37df64f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingExerciseResource.java @@ -51,6 +51,7 @@ import de.tum.in.www1.artemis.service.ExerciseService; import de.tum.in.www1.artemis.service.ModelingExerciseImportService; import de.tum.in.www1.artemis.service.ModelingExerciseService; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.export.SubmissionExportService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; @@ -80,6 +81,8 @@ public class ModelingExerciseResource { private static final String ENTITY_NAME = "modelingExercise"; + private final CompetencyProgressService competencyProgressService; + @Value("${jhipster.clientApp.name}") private String applicationName; @@ -122,7 +125,8 @@ public ModelingExerciseResource(ModelingExerciseRepository modelingExerciseRepos ModelingExerciseService modelingExerciseService, ExerciseDeletionService exerciseDeletionService, PlagiarismResultRepository plagiarismResultRepository, ModelingExerciseImportService modelingExerciseImportService, SubmissionExportService modelingSubmissionExportService, ExerciseService exerciseService, GroupNotificationScheduleService groupNotificationScheduleService, GradingCriterionRepository gradingCriterionRepository, - PlagiarismDetectionService plagiarismDetectionService, ChannelService channelService, ChannelRepository channelRepository) { + PlagiarismDetectionService plagiarismDetectionService, ChannelService channelService, ChannelRepository channelRepository, + CompetencyProgressService competencyProgressService) { this.modelingExerciseRepository = modelingExerciseRepository; this.courseService = courseService; this.modelingExerciseService = modelingExerciseService; @@ -140,6 +144,7 @@ public ModelingExerciseResource(ModelingExerciseRepository modelingExerciseRepos this.plagiarismDetectionService = plagiarismDetectionService; this.channelService = channelService; this.channelRepository = channelRepository; + this.competencyProgressService = competencyProgressService; } // TODO: most of these calls should be done in the context of a course @@ -177,6 +182,7 @@ public ResponseEntity createModelingExercise(@RequestBody Mode channelService.createExerciseChannel(result, Optional.ofNullable(modelingExercise.getChannelName())); modelingExerciseService.scheduleOperations(result.getId()); groupNotificationScheduleService.checkNotificationsForNewExerciseAsync(modelingExercise); + competencyProgressService.updateProgressByLearningObjectAsync(result); return ResponseEntity.created(new URI("/api/modeling-exercises/" + result.getId())).body(result); } @@ -223,7 +229,7 @@ public ResponseEntity updateModelingExercise(@RequestBody Mode // Check that the user is authorized to update the exercise var user = userRepository.getUserWithGroupsAndAuthorities(); // Important: use the original exercise for permission check - final ModelingExercise modelingExerciseBeforeUpdate = modelingExerciseRepository.findByIdElseThrow(modelingExercise.getId()); + final ModelingExercise modelingExerciseBeforeUpdate = modelingExerciseRepository.findWithEagerCompetenciesByIdElseThrow(modelingExercise.getId()); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, modelingExerciseBeforeUpdate, user); // Forbid changing the course the exercise belongs to. @@ -246,6 +252,8 @@ public ResponseEntity updateModelingExercise(@RequestBody Mode exerciseService.notifyAboutExerciseChanges(modelingExerciseBeforeUpdate, updatedModelingExercise, notificationText); + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(modelingExerciseBeforeUpdate, Optional.of(modelingExercise)); + return ResponseEntity.ok(updatedModelingExercise); } @@ -349,7 +357,7 @@ public ResponseEntity importExercise(@PathVariable long source log.debug("Either the courseId or exerciseGroupId must be set for an import"); throw new BadRequestAlertException("Either the courseId or exerciseGroupId must be set for an import", ENTITY_NAME, "noCourseIdOrExerciseGroupId"); } - importedExercise.checkCourseAndExerciseGroupExclusivity("Modeling Exercise"); + importedExercise.checkCourseAndExerciseGroupExclusivity(ENTITY_NAME); final var user = userRepository.getUserWithGroupsAndAuthorities(); final var originalModelingExercise = modelingExerciseRepository.findByIdWithExampleSubmissionsAndResultsAndPlagiarismDetectionConfigElseThrow(sourceExerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, importedExercise, user); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java index a19d7e83b43e..793d9c49fb45 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizExerciseResource.java @@ -69,6 +69,7 @@ import de.tum.in.www1.artemis.service.ExerciseService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.exam.ExamDateService; import de.tum.in.www1.artemis.service.messaging.InstanceMessageSendService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; @@ -100,13 +101,13 @@ public class QuizExerciseResource { private static final String ENTITY_NAME = "quizExercise"; + @Value("${jhipster.clientApp.name}") + private String applicationName; + private final QuizSubmissionService quizSubmissionService; private final QuizResultService quizResultService; - @Value("${jhipster.clientApp.name}") - private String applicationName; - private final QuizExerciseService quizExerciseService; private final QuizMessagingService quizMessagingService; @@ -147,13 +148,15 @@ public class QuizExerciseResource { private final ChannelRepository channelRepository; + private final CompetencyProgressService competencyProgressService; + public QuizExerciseResource(QuizExerciseService quizExerciseService, QuizMessagingService quizMessagingService, QuizExerciseRepository quizExerciseRepository, UserRepository userRepository, CourseService courseService, ExerciseService exerciseService, ExerciseDeletionService exerciseDeletionService, ExamDateService examDateService, InstanceMessageSendService instanceMessageSendService, QuizStatisticService quizStatisticService, QuizExerciseImportService quizExerciseImportService, AuthorizationCheckService authCheckService, GroupNotificationService groupNotificationService, GroupNotificationScheduleService groupNotificationScheduleService, StudentParticipationRepository studentParticipationRepository, QuizBatchService quizBatchService, QuizBatchRepository quizBatchRepository, FileService fileService, ChannelService channelService, ChannelRepository channelRepository, - QuizSubmissionService quizSubmissionService, QuizResultService quizResultService) { + QuizSubmissionService quizSubmissionService, QuizResultService quizResultService, CompetencyProgressService competencyProgressService) { this.quizExerciseService = quizExerciseService; this.quizMessagingService = quizMessagingService; this.quizExerciseRepository = quizExerciseRepository; @@ -176,6 +179,7 @@ public QuizExerciseResource(QuizExerciseService quizExerciseService, QuizMessagi this.channelRepository = channelRepository; this.quizSubmissionService = quizSubmissionService; this.quizResultService = quizResultService; + this.competencyProgressService = competencyProgressService; } /** @@ -237,6 +241,8 @@ public ResponseEntity createQuizExercise(@RequestPart("exercise") channelService.createExerciseChannel(result, Optional.ofNullable(quizExercise.getChannelName())); + competencyProgressService.updateProgressByLearningObjectAsync(result); + return ResponseEntity.created(new URI("/api/quiz-exercises/" + result.getId())) .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.getId().toString())).body(result); } @@ -270,7 +276,7 @@ public ResponseEntity updateQuizExercise(@PathVariable Long exerci // Valid exercises have set either a course or an exerciseGroup quizExercise.checkCourseAndExerciseGroupExclusivity(ENTITY_NAME); - final var originalQuiz = quizExerciseRepository.findWithEagerQuestionsByIdOrElseThrow(exerciseId); + final var originalQuiz = quizExerciseRepository.findByIdWithQuestionsAndCompetenciesElseThrow(exerciseId); var user = userRepository.getUserWithGroupsAndAuthorities(); @@ -300,6 +306,8 @@ public ResponseEntity updateQuizExercise(@PathVariable Long exerci if (updatedChannel != null) { quizExercise.setChannelName(updatedChannel.getName()); } + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(originalQuiz, Optional.of(quizExercise)); + return ResponseEntity.ok(quizExercise); } @@ -635,7 +643,7 @@ public ResponseEntity evaluateQuizExercise(@PathVariable Long quizExercise @EnforceAtLeastInstructorInExercise(resourceIdFieldName = "quizExerciseId") public ResponseEntity deleteQuizExercise(@PathVariable Long quizExerciseId) { log.info("REST request to delete quiz exercise : {}", quizExerciseId); - var quizExercise = quizExerciseRepository.findWithEagerQuestionsByIdOrElseThrow(quizExerciseId); + var quizExercise = quizExerciseRepository.findByIdWithQuestionsAndCompetenciesElseThrow(quizExerciseId); var user = userRepository.getUserWithGroupsAndAuthorities(); List dragAndDropQuestions = quizExercise.getQuizQuestions().stream().filter(question -> question instanceof DragAndDropQuestion) @@ -769,6 +777,7 @@ public ResponseEntity importExercise(@PathVariable long sourceExer final var originalQuizExercise = quizExerciseRepository.findByIdElseThrow(sourceExerciseId); final var newQuizExercise = quizExerciseImportService.importQuizExercise(originalQuizExercise, importedExercise); + return ResponseEntity.created(new URI("/api/quiz-exercises/" + newQuizExercise.getId())) .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, newQuizExercise.getId().toString())).body(newQuizExercise); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java index 21e756d57914..38e84f12e844 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java @@ -68,6 +68,7 @@ import de.tum.in.www1.artemis.service.ExerciseService; import de.tum.in.www1.artemis.service.TextExerciseImportService; import de.tum.in.www1.artemis.service.TextExerciseService; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.connectors.athena.AthenaModuleService; import de.tum.in.www1.artemis.service.export.TextSubmissionExportService; import de.tum.in.www1.artemis.service.feature.Feature; @@ -152,6 +153,8 @@ public class TextExerciseResource { private final Optional athenaModuleService; + private final CompetencyProgressService competencyProgressService; + public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextExerciseService textExerciseService, FeedbackRepository feedbackRepository, ExerciseDeletionService exerciseDeletionService, PlagiarismResultRepository plagiarismResultRepository, UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService, StudentParticipationRepository studentParticipationRepository, @@ -159,7 +162,8 @@ public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextE TextSubmissionExportService textSubmissionExportService, ExampleSubmissionRepository exampleSubmissionRepository, ExerciseService exerciseService, GradingCriterionRepository gradingCriterionRepository, TextBlockRepository textBlockRepository, GroupNotificationScheduleService groupNotificationScheduleService, InstanceMessageSendService instanceMessageSendService, PlagiarismDetectionService plagiarismDetectionService, CourseRepository courseRepository, - ChannelService channelService, ChannelRepository channelRepository, Optional athenaModuleService) { + ChannelService channelService, ChannelRepository channelRepository, Optional athenaModuleService, + CompetencyProgressService competencyProgressService) { this.feedbackRepository = feedbackRepository; this.exerciseDeletionService = exerciseDeletionService; this.plagiarismResultRepository = plagiarismResultRepository; @@ -184,6 +188,7 @@ public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextE this.channelService = channelService; this.channelRepository = channelRepository; this.athenaModuleService = athenaModuleService; + this.competencyProgressService = competencyProgressService; } /** @@ -223,6 +228,8 @@ public ResponseEntity createTextExercise(@RequestBody TextExercise channelService.createExerciseChannel(result, Optional.ofNullable(textExercise.getChannelName())); instanceMessageSendService.sendTextExerciseSchedule(result.getId()); groupNotificationScheduleService.checkNotificationsForNewExerciseAsync(textExercise); + competencyProgressService.updateProgressByLearningObjectAsync(result); + return ResponseEntity.created(new URI("/api/text-exercises/" + result.getId())).body(result); } @@ -253,7 +260,7 @@ public ResponseEntity updateTextExercise(@RequestBody TextExercise // Check that the user is authorized to update the exercise var user = userRepository.getUserWithGroupsAndAuthorities(); // Important: use the original exercise for permission check - final TextExercise textExerciseBeforeUpdate = textExerciseRepository.findByIdElseThrow(textExercise.getId()); + final TextExercise textExerciseBeforeUpdate = textExerciseRepository.findWithEagerCompetenciesByIdElseThrow(textExercise.getId()); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, textExerciseBeforeUpdate, user); // Forbid changing the course the exercise belongs to. @@ -280,6 +287,8 @@ public ResponseEntity updateTextExercise(@RequestBody TextExercise exerciseService.checkExampleSubmissions(updatedTextExercise); exerciseService.notifyAboutExerciseChanges(textExerciseBeforeUpdate, updatedTextExercise, notificationText); + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(textExerciseBeforeUpdate, Optional.of(textExercise)); + return ResponseEntity.ok(updatedTextExercise); } @@ -491,7 +500,7 @@ public ResponseEntity importExercise(@PathVariable long sourceExer log.debug("Either the courseId or exerciseGroupId must be set for an import"); throw new BadRequestAlertException("Either the courseId or exerciseGroupId must be set for an import", ENTITY_NAME, "noCourseIdOrExerciseGroupId"); } - importedExercise.checkCourseAndExerciseGroupExclusivity("Text Exercise"); + importedExercise.checkCourseAndExerciseGroupExclusivity(ENTITY_NAME); final var user = userRepository.getUserWithGroupsAndAuthorities(); final var originalTextExercise = textExerciseRepository.findByIdWithExampleSubmissionsAndResultsElseThrow(sourceExerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, importedExercise, user); @@ -511,6 +520,7 @@ public ResponseEntity importExercise(@PathVariable long sourceExer final var newTextExercise = textExerciseImportService.importTextExercise(originalTextExercise, importedExercise); textExerciseRepository.save(newTextExercise); + return ResponseEntity.created(new URI("/api/text-exercises/" + newTextExercise.getId())).body(newTextExercise); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java index 5ee5bb9c5808..231e4ec20ae7 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/competency/CompetencyResource.java @@ -421,7 +421,7 @@ public ResponseEntity deleteCompetency(@PathVariable long competencyId, @P @GetMapping("courses/{courseId}/competencies/{competencyId}/student-progress") @EnforceAtLeastStudentInCourse public ResponseEntity getCompetencyStudentProgress(@PathVariable long courseId, @PathVariable long competencyId, - @RequestParam(defaultValue = "false") Boolean refresh) { + @RequestParam(defaultValue = "false") boolean refresh) { log.debug("REST request to get student progress for competency: {}", competencyId); var user = userRepository.getUserWithGroupsAndAuthorities(); var course = courseRepository.findByIdElseThrow(courseId); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/AttachmentUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/AttachmentUnitResource.java index 66bc1dd1ead0..9f0bd52376e9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/AttachmentUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/AttachmentUnitResource.java @@ -125,7 +125,7 @@ public ResponseEntity updateAttachmentUnit(@PathVariable Long le @RequestPart Attachment attachment, @RequestPart(required = false) MultipartFile file, @RequestParam(defaultValue = "false") boolean keepFilename, @RequestParam(value = "notificationText", required = false) String notificationText) { log.debug("REST request to update an attachment unit : {}", attachmentUnit); - AttachmentUnit existingAttachmentUnit = attachmentUnitRepository.findOneWithSlides(attachmentUnitId); + AttachmentUnit existingAttachmentUnit = attachmentUnitRepository.findOneWithSlidesAndCompetencies(attachmentUnitId); checkAttachmentUnitCourseAndLecture(existingAttachmentUnit, lectureId); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, existingAttachmentUnit.getLecture().getCourse(), null); @@ -135,8 +135,6 @@ public ResponseEntity updateAttachmentUnit(@PathVariable Long le groupNotificationService.notifyStudentGroupAboutAttachmentChange(savedAttachmentUnit.getAttachment(), notificationText); } - competencyProgressService.updateProgressByLearningObjectAsync(savedAttachmentUnit); - return ResponseEntity.ok(savedAttachmentUnit); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/LectureUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/LectureUnitResource.java index 565a0c504fbe..3e3e589c4203 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/LectureUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/LectureUnitResource.java @@ -142,7 +142,7 @@ public ResponseEntity completeLectureUnit(@PathVariable Long lectureUnitId authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, lectureUnit.getLecture().getCourse(), user); lectureUnitService.setLectureUnitCompletion(lectureUnit, user, completed); - competencyProgressService.updateProgressByLearningObjectAsync(lectureUnit, user); + competencyProgressService.updateProgressByLearningObjectForParticipantAsync(lectureUnit, user); return ResponseEntity.ok().build(); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/OnlineUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/OnlineUnitResource.java index e5acc62bde19..1db202306ae0 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/OnlineUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/OnlineUnitResource.java @@ -6,6 +6,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Optional; import jakarta.ws.rs.BadRequestException; @@ -100,13 +101,17 @@ public ResponseEntity updateOnlineUnit(@PathVariable Long lectureId, throw new BadRequestException(); } - checkOnlineUnitCourseAndLecture(onlineUnit, lectureId); + var existingOnlineUnit = onlineUnitRepository.findByIdWithCompetenciesElseThrow(onlineUnit.getId()); + + checkOnlineUnitCourseAndLecture(existingOnlineUnit, lectureId); lectureUnitService.validateUrlStringAndReturnUrl(onlineUnit.getSource()); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, onlineUnit.getLecture().getCourse(), null); OnlineUnit result = onlineUnitRepository.save(onlineUnit); - competencyProgressService.updateProgressByLearningObjectAsync(result); + + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(existingOnlineUnit, Optional.of(onlineUnit)); + return ResponseEntity.ok(result); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/TextUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/TextUnitResource.java index 64f1faba8c72..9dae07a3feab 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/TextUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/TextUnitResource.java @@ -92,18 +92,18 @@ public ResponseEntity updateTextUnit(@PathVariable Long lectureId, @Re throw new BadRequestAlertException("A text unit must have an ID to be updated", ENTITY_NAME, "idNull"); } - var textUnit = textUnitRepository.findByIdWithCompetenciesBidirectional(textUnitForm.getId()).orElseThrow(); + var existingTextUnit = textUnitRepository.findByIdWithCompetencies(textUnitForm.getId()).orElseThrow(); - if (textUnit.getLecture() == null || textUnit.getLecture().getCourse() == null || !textUnit.getLecture().getId().equals(lectureId)) { + if (existingTextUnit.getLecture() == null || existingTextUnit.getLecture().getCourse() == null || !existingTextUnit.getLecture().getId().equals(lectureId)) { throw new BadRequestAlertException("Input data not valid", ENTITY_NAME, "inputInvalid"); } - authorizationCheckService.checkHasAtLeastRoleForLectureElseThrow(Role.EDITOR, textUnit.getLecture(), null); + authorizationCheckService.checkHasAtLeastRoleForLectureElseThrow(Role.EDITOR, existingTextUnit.getLecture(), null); - textUnitForm.setId(textUnit.getId()); - textUnitForm.setLecture(textUnit.getLecture()); + textUnitForm.setId(existingTextUnit.getId()); + textUnitForm.setLecture(existingTextUnit.getLecture()); TextUnit result = textUnitRepository.save(textUnitForm); - competencyProgressService.updateProgressByLearningObjectAsync(result); + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(existingTextUnit, Optional.of(textUnitForm)); return ResponseEntity.ok(result); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/VideoUnitResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/VideoUnitResource.java index 90ebec0d8eb3..026611ad945c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/VideoUnitResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/lecture/VideoUnitResource.java @@ -4,6 +4,7 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.Optional; import jakarta.ws.rs.BadRequestException; @@ -89,15 +90,16 @@ public ResponseEntity updateVideoUnit(@PathVariable Long lectureId, @ if (videoUnit.getId() == null) { throw new BadRequestException(); } + var existingVideoUnit = videoUnitRepository.findByIdWithCompetenciesElseThrow(videoUnit.getId()); - checkVideoUnitCourseAndLecture(videoUnit, lectureId); + checkVideoUnitCourseAndLecture(existingVideoUnit, lectureId); normalizeVideoUrl(videoUnit); lectureUnitService.validateUrlStringAndReturnUrl(videoUnit.getSource()); authorizationCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, videoUnit.getLecture().getCourse(), null); VideoUnit result = videoUnitRepository.save(videoUnit); - competencyProgressService.updateProgressByLearningObjectAsync(result); + competencyProgressService.updateProgressForUpdatedLearningObjectAsync(existingVideoUnit, Optional.of(videoUnit)); return ResponseEntity.ok(result); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java index 21c0de2c44b9..b49c48a50723 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java @@ -62,6 +62,7 @@ import de.tum.in.www1.artemis.service.ConsistencyCheckService; import de.tum.in.www1.artemis.service.CourseService; import de.tum.in.www1.artemis.service.SubmissionPolicyService; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.connectors.athena.AthenaModuleService; import de.tum.in.www1.artemis.service.exam.ExamAccessService; import de.tum.in.www1.artemis.service.export.ProgrammingExerciseExportService; @@ -93,6 +94,8 @@ public class ProgrammingExerciseExportImportResource { private static final String ENTITY_NAME = "programmingExercise"; + private final CompetencyProgressService competencyProgressService; + @Value("${jhipster.clientApp.name}") private String applicationName; @@ -132,7 +135,7 @@ public ProgrammingExerciseExportImportResource(ProgrammingExerciseRepository pro AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, SubmissionPolicyService submissionPolicyService, ProgrammingExerciseTaskRepository programmingExerciseTaskRepository, ExamAccessService examAccessService, CourseRepository courseRepository, ProgrammingExerciseImportFromFileService programmingExerciseImportFromFileService, ConsistencyCheckService consistencyCheckService, - Optional athenaModuleService) { + Optional athenaModuleService, CompetencyProgressService competencyProgressService) { this.programmingExerciseRepository = programmingExerciseRepository; this.userRepository = userRepository; this.courseService = courseService; @@ -148,6 +151,7 @@ public ProgrammingExerciseExportImportResource(ProgrammingExerciseRepository pro this.programmingExerciseImportFromFileService = programmingExerciseImportFromFileService; this.consistencyCheckService = consistencyCheckService; this.athenaModuleService = athenaModuleService; + this.competencyProgressService = competencyProgressService; } /** @@ -254,6 +258,8 @@ public ResponseEntity importProgrammingExercise(@PathVariab importedProgrammingExercise.setExerciseHints(null); importedProgrammingExercise.setTasks(null); + competencyProgressService.updateProgressByLearningObjectAsync(importedProgrammingExercise); + return ResponseEntity.ok().headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, importedProgrammingExercise.getTitle())) .body(importedProgrammingExercise); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java index 7e24cd235db5..051ecc7f2ea8 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java @@ -102,11 +102,11 @@ public class ProgrammingExerciseResource { private static final String ENTITY_NAME = "programmingExercise"; - private final ChannelRepository channelRepository; - @Value("${jhipster.clientApp.name}") private String applicationName; + private final ChannelRepository channelRepository; + private final ProgrammingExerciseRepository programmingExerciseRepository; private final ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository; @@ -291,7 +291,7 @@ public ResponseEntity updateProgrammingExercise(@RequestBod checkProgrammingExerciseForError(updatedProgrammingExercise); - var programmingExerciseBeforeUpdate = programmingExerciseRepository.findByIdWithAuxiliaryRepositoriesElseThrow(updatedProgrammingExercise.getId()); + var programmingExerciseBeforeUpdate = programmingExerciseRepository.findByIdWithAuxiliaryRepositoriesAndCompetenciesElseThrow(updatedProgrammingExercise.getId()); if (!Objects.equals(programmingExerciseBeforeUpdate.getShortName(), updatedProgrammingExercise.getShortName())) { throw new BadRequestAlertException("The programming exercise short name cannot be changed", ENTITY_NAME, "shortNameCannotChange"); } @@ -346,6 +346,7 @@ public ResponseEntity updateProgrammingExercise(@RequestBod exerciseService.logUpdate(updatedProgrammingExercise, updatedProgrammingExercise.getCourseViaExerciseGroupOrCourseMember(), user); exerciseService.updatePointsInRelatedParticipantScores(programmingExerciseBeforeUpdate, updatedProgrammingExercise); + return ResponseEntity.ok(savedProgrammingExercise); } @@ -520,7 +521,7 @@ public ResponseEntity getProgrammingExerciseWithTemplateAnd public ResponseEntity deleteProgrammingExercise(@PathVariable long exerciseId, @RequestParam(defaultValue = "true") boolean deleteStudentReposBuildPlans, @RequestParam(defaultValue = "true") boolean deleteBaseReposBuildPlans) { log.info("REST request to delete ProgrammingExercise : {}", exerciseId); - var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesElseThrow(exerciseId); + var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndCompetenciesElseThrow(exerciseId); User user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, programmingExercise, user); exerciseService.logDeletion(programmingExercise, programmingExercise.getCourseViaExerciseGroupOrCourseMember(), user); diff --git a/src/main/webapp/app/entities/exercise.model.ts b/src/main/webapp/app/entities/exercise.model.ts index fa285e0a6e60..0573d622ff9f 100644 --- a/src/main/webapp/app/entities/exercise.model.ts +++ b/src/main/webapp/app/entities/exercise.model.ts @@ -270,7 +270,7 @@ export function getExerciseUrlSegment(exerciseType?: ExerciseType): string { } } -export function resetDates(exercise: Exercise) { +export function resetForImport(exercise: Exercise) { exercise.releaseDate = undefined; exercise.startDate = undefined; exercise.dueDate = undefined; @@ -280,4 +280,6 @@ export function resetDates(exercise: Exercise) { // without dates set, they can only be false exercise.allowComplaintsForAutomaticAssessments = false; exercise.allowFeedbackRequests = false; + + exercise.competencies = []; } diff --git a/src/main/webapp/app/entities/programming-exercise.model.ts b/src/main/webapp/app/entities/programming-exercise.model.ts index e14bc12a0beb..5e36139c11cd 100644 --- a/src/main/webapp/app/entities/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming-exercise.model.ts @@ -1,7 +1,7 @@ import dayjs from 'dayjs/esm'; import { SolutionProgrammingExerciseParticipation } from 'app/entities/participation/solution-programming-exercise-participation.model'; import { TemplateProgrammingExerciseParticipation } from 'app/entities/participation/template-programming-exercise-participation.model'; -import { Exercise, ExerciseType, resetDates } from 'app/entities/exercise.model'; +import { Exercise, ExerciseType, resetForImport } from 'app/entities/exercise.model'; import { Course } from 'app/entities/course.model'; import { ExerciseGroup } from 'app/entities/exercise-group.model'; import { AuxiliaryRepository } from 'app/entities/programming-exercise-auxiliary-repository-model'; @@ -9,6 +9,7 @@ import { SubmissionPolicy } from 'app/entities/submission-policy.model'; import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; import { ExerciseHint } from 'app/entities/hestia/exercise-hint.model'; import { BuildLogStatisticsDTO } from 'app/entities/build-log-statistics-dto'; +import { AssessmentType } from 'app/entities/assessment-type.model'; export class BuildAction { name: string; @@ -146,10 +147,11 @@ export class ProgrammingExercise extends Exercise { } } -export function resetProgrammingDates(exercise: ProgrammingExercise) { - resetDates(exercise); +export function resetProgrammingForImport(exercise: ProgrammingExercise) { + resetForImport(exercise); // without dates set, they have to be reset as well exercise.releaseTestsWithExampleSolution = false; exercise.buildAndTestStudentSubmissionsAfterDueDate = undefined; + exercise.assessmentType = AssessmentType.AUTOMATIC; } diff --git a/src/main/webapp/app/entities/quiz/quiz-exercise.model.ts b/src/main/webapp/app/entities/quiz/quiz-exercise.model.ts index 247aa6f9b11c..5033f2ac024f 100644 --- a/src/main/webapp/app/entities/quiz/quiz-exercise.model.ts +++ b/src/main/webapp/app/entities/quiz/quiz-exercise.model.ts @@ -1,5 +1,5 @@ import dayjs from 'dayjs/esm'; -import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { Exercise, ExerciseType, resetForImport } from 'app/entities/exercise.model'; import { QuizPointStatistic } from 'app/entities/quiz/quiz-point-statistic.model'; import { QuizQuestion } from 'app/entities/quiz/quiz-question.model'; import { Course } from 'app/entities/course.model'; @@ -65,3 +65,10 @@ export class QuizExercise extends Exercise implements QuizConfiguration, QuizPar this.isEditable = false; // default value (set by client, might need to be computed before evaluated) } } + +export function resetQuizForImport(exercise: QuizExercise) { + resetForImport(exercise); + + exercise.quizBatches = []; + exercise.isEditable = true; +} diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.ts b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.ts index 2188e61d7f65..38e688e7c415 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.ts @@ -6,7 +6,7 @@ import { FileUploadExerciseService } from './file-upload-exercise.service'; import { FileUploadExercise } from 'app/entities/file-upload-exercise.model'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { Exercise, ExerciseMode, IncludedInOverallScore, getCourseId, resetDates } from 'app/entities/exercise.model'; +import { Exercise, ExerciseMode, IncludedInOverallScore, getCourseId, resetForImport } from 'app/entities/exercise.model'; import { EditorMode } from 'app/shared/markdown-editor/markdown-editor.component'; import { KatexCommand } from 'app/shared/markdown-editor/commands/katex.command'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; @@ -204,7 +204,7 @@ export class FileUploadExerciseUpdateComponent implements AfterViewInit, OnDestr // We reference normal exercises by their course, having both would lead to conflicts on the server this.fileUploadExercise.exerciseGroup = undefined; } - resetDates(this.fileUploadExercise); + resetForImport(this.fileUploadExercise); } } diff --git a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.ts b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.ts index 1445f8c8b5e2..7e66dac4b5d8 100644 --- a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise-update.component.ts @@ -5,7 +5,7 @@ import { ModelingExercise } from 'app/entities/modeling-exercise.model'; import { ModelingExerciseService } from './modeling-exercise.service'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { ExerciseMode, IncludedInOverallScore, resetDates } from 'app/entities/exercise.model'; +import { ExerciseMode, IncludedInOverallScore, resetForImport } from 'app/entities/exercise.model'; import { EditorMode } from 'app/shared/markdown-editor/markdown-editor.component'; import { KatexCommand } from 'app/shared/markdown-editor/commands/katex.command'; import { AssessmentType } from 'app/entities/assessment-type.model'; @@ -176,7 +176,7 @@ export class ModelingExerciseUpdateComponent implements AfterViewInit, OnDestroy // We reference normal exercises by their course, having both would lead to conflicts on the server this.modelingExercise.exerciseGroup = undefined; } - resetDates(this.modelingExercise); + resetForImport(this.modelingExercise); } loadCourseExerciseCategories(courseId, this.courseService, this.exerciseService, this.alertService).subscribe((existingCategories) => { diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts index 9ec0d4cdc801..4f768aa697a2 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts @@ -4,7 +4,7 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService, AlertType } from 'app/core/util/alert.service'; import { Observable, Subject, Subscription } from 'rxjs'; import { CourseManagementService } from 'app/course/manage/course-management.service'; -import { ProgrammingExercise, ProgrammingLanguage, ProjectType, resetProgrammingDates } from 'app/entities/programming-exercise.model'; +import { ProgrammingExercise, ProgrammingLanguage, ProjectType, resetProgrammingForImport } from 'app/entities/programming-exercise.model'; import { ProgrammingExerciseService } from '../services/programming-exercise.service'; import { FileService } from 'app/shared/http/file.service'; import { TranslateService } from '@ngx-translate/core'; @@ -560,7 +560,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest this.isExamMode = false; } this.loadCourseExerciseCategories(courseId); - resetProgrammingDates(this.programmingExercise); + resetProgrammingForImport(this.programmingExercise); this.programmingExercise.projectKey = undefined; if (this.programmingExercise.submissionPolicy) { @@ -1056,7 +1056,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest this.programmingExercise.course = undefined; this.programmingExercise.projectKey = undefined; - resetProgrammingDates(this.programmingExercise); + resetProgrammingForImport(this.programmingExercise); this.selectedProgrammingLanguage = this.programmingExercise.programmingLanguage!; // we need to get it from the history object as setting the programming language diff --git a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-update.component.ts b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-update.component.ts index 754cfbdd7d7a..c8de2cbc31b0 100644 --- a/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/quiz/manage/quiz-exercise-update.component.ts @@ -3,7 +3,7 @@ import { QuizExerciseService } from './quiz-exercise.service'; import { ActivatedRoute, Router } from '@angular/router'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { CourseManagementService } from 'app/course/manage/course-management.service'; -import { QuizBatch, QuizExercise, QuizMode } from 'app/entities/quiz/quiz-exercise.model'; +import { QuizBatch, QuizExercise, QuizMode, resetQuizForImport } from 'app/entities/quiz/quiz-exercise.model'; import { DragAndDropQuestionUtil } from 'app/exercises/quiz/shared/drag-and-drop-question-util.service'; import { ShortAnswerQuestionUtil } from 'app/exercises/quiz/shared/short-answer-question-util.service'; import { TranslateService } from '@ngx-translate/core'; @@ -13,7 +13,7 @@ import dayjs from 'dayjs/esm'; import { AlertService } from 'app/core/util/alert.service'; import { ComponentCanDeactivate } from 'app/shared/guard/can-deactivate.model'; import { QuizQuestion } from 'app/entities/quiz/quiz-question.model'; -import { Exercise, IncludedInOverallScore, ValidationReason, resetDates } from 'app/entities/exercise.model'; +import { Exercise, IncludedInOverallScore, ValidationReason } from 'app/entities/exercise.model'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { Course } from 'app/entities/course.model'; import { ExerciseGroupService } from 'app/exam/manage/exercise-groups/exercise-group.service'; @@ -214,9 +214,7 @@ export class QuizExerciseUpdateComponent extends QuizExerciseValidationDirective } if (this.isImport || this.isExamMode) { - this.quizExercise.quizBatches = []; - this.quizExercise.isEditable = true; - resetDates(this.quizExercise); + resetQuizForImport(this.quizExercise); } if (this.isExamMode) { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts index 9fec05990b65..21011cacb571 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.ts @@ -6,7 +6,7 @@ import { TextExerciseService } from './text-exercise.service'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AssessmentType } from 'app/entities/assessment-type.model'; -import { ExerciseMode, IncludedInOverallScore, resetDates } from 'app/entities/exercise.model'; +import { ExerciseMode, IncludedInOverallScore, resetForImport } from 'app/entities/exercise.model'; import { EditorMode } from 'app/shared/markdown-editor/markdown-editor.component'; import { KatexCommand } from 'app/shared/markdown-editor/commands/katex.command'; import { switchMap, tap } from 'rxjs/operators'; @@ -167,7 +167,7 @@ export class TextExerciseUpdateComponent implements OnInit, OnDestroy, AfterView } this.loadCourseExerciseCategories(courseId); - resetDates(this.textExercise); + resetForImport(this.textExercise); } }), ) diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractArtemisIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractArtemisIntegrationTest.java index 857e918cb967..70a73254ca01 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractArtemisIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractArtemisIntegrationTest.java @@ -39,6 +39,7 @@ import de.tum.in.www1.artemis.service.UriService; import de.tum.in.www1.artemis.service.WebsocketMessagingService; import de.tum.in.www1.artemis.service.ZipFileService; +import de.tum.in.www1.artemis.service.competency.CompetencyProgressService; import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.connectors.lti.Lti13Service; import de.tum.in.www1.artemis.service.exam.ExamAccessService; @@ -160,6 +161,9 @@ public abstract class AbstractArtemisIntegrationTest implements MockDelegate { @SpyBean protected TextBlockService textBlockService; + @SpyBean + protected CompetencyProgressService competencyProgressService; + @Autowired protected RequestUtilService request; @@ -205,7 +209,7 @@ void stopRunningTasks() { protected void resetSpyBeans() { Mockito.reset(gitService, groupNotificationService, conversationNotificationService, tutorialGroupNotificationService, singleUserNotificationService, websocketMessagingService, examAccessService, mailService, instanceMessageSendService, programmingExerciseScheduleService, programmingExerciseParticipationService, - uriService, scheduleService, participantScoreScheduleService, javaMailSender, programmingTriggerService, zipFileService); + uriService, scheduleService, participantScoreScheduleService, javaMailSender, programmingTriggerService, zipFileService, competencyProgressService); } @Override diff --git a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyIntegrationTest.java index 065ceefb3fdb..ef9cf6c1e447 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/CompetencyIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/CompetencyIntegrationTest.java @@ -3,6 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.within; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import java.time.ZonedDateTime; import java.util.Collections; @@ -794,6 +797,7 @@ void shouldCreateValidCompetency() throws Exception { var persistedCompetency = request.postWithResponseBody("/api/courses/" + course.getId() + "/competencies", competency, Competency.class, HttpStatus.CREATED); assertThat(persistedCompetency.getId()).isNotNull(); + verify(competencyProgressService).updateProgressByCompetencyAndUsersInCourseAsync(eq(persistedCompetency)); } @Nested @@ -850,6 +854,21 @@ void shouldImportCompetency() throws Exception { assertThat(importedCompetency.getExercises()).isEmpty(); assertThat(importedCompetency.getLectureUnits()).isEmpty(); assertThat(importedCompetency.getUserProgress()).isEmpty(); + verify(competencyProgressService, never()).updateProgressByCompetencyAsync(importedCompetency); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void shouldImportCompetencyWithLectureUnits() throws Exception { + List allLectureUnits = lectureUnitRepository.findAll(); + Set connectedLectureUnits = new HashSet<>(allLectureUnits); + competency.setLectureUnits(connectedLectureUnits); + Competency importedCompetency = request.postWithResponseBody("/api/courses/" + course2.getId() + "/competencies/import", competency, Competency.class, + HttpStatus.CREATED); + + assertThat(competencyRepository.findById(importedCompetency.getId())).isNotEmpty(); + assertThat(importedCompetency.getLectureUnits()).containsExactlyInAnyOrderElementsOf(connectedLectureUnits); + verify(competencyProgressService).updateProgressByCompetencyAndUsersInCourseAsync(eq(importedCompetency)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java index 695d55ad32d1..4143656c4e31 100644 --- a/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/competency/LearningPathIntegrationTest.java @@ -1,6 +1,8 @@ package de.tum.in.www1.artemis.competency; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; import java.time.ZonedDateTime; import java.util.Arrays; @@ -409,11 +411,7 @@ void testUpdateLearningPathProgress() throws Exception { var learningPath = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); assertThat(learningPath.getProgress()).as("contains no completed competency").isEqualTo(0); - // force update to avoid waiting for scheduler - competencyProgressService.updateCompetencyProgress(createdCompetency.getId(), student); - - learningPath = learningPathRepository.findWithEagerCompetenciesByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); - assertThat(learningPath.getProgress()).as("contains completed competency").isNotEqualTo(0); + verify(competencyProgressService).updateProgressByCompetencyAndUsersInCourseAsync(eq(createdCompetency)); } /** @@ -650,7 +648,7 @@ void testGetLearningPathNavigation() throws Exception { final var student = userRepository.findOneByLogin(STUDENT_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); - competencyProgressService.updateProgressByLearningObject(textUnit, Set.of(student)); + competencyProgressService.updateProgressByLearningObjectSync(textUnit, Set.of(student)); final var result = request.get("/api/learning-path/" + learningPath.getId() + "/navigation", HttpStatus.OK, LearningPathNavigationDTO.class); diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java index fbfc0a71bf61..d54bf231ac50 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java @@ -1754,7 +1754,7 @@ void testImportExamWithQuizExercise_successfulWithQuestions() throws Exception { // The directly returned exam should not contain details like the quiz questions assertThat(exercise.getQuizQuestions()).isEmpty(); - exercise = quizExerciseRepository.findWithEagerQuestionsByIdOrElseThrow(exercise.getId()); + exercise = quizExerciseRepository.findByIdWithQuestionsElseThrow(exercise.getId()); // Quiz questions should get imported into the exam assertThat(exercise.getQuizQuestions()).hasSize(3); } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadAssessmentIntegrationTest.java index 01de16a0389f..32508188e37a 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadAssessmentIntegrationTest.java @@ -14,10 +14,7 @@ import org.assertj.core.data.Offset; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; @@ -57,7 +54,6 @@ import de.tum.in.www1.artemis.web.rest.dto.FileUploadAssessmentDTO; import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class FileUploadAssessmentIntegrationTest extends AbstractSpringIntegrationIndependentTest { private static final String TEST_PREFIX = "fileuploadassessment"; @@ -109,7 +105,6 @@ private List exerciseWithSGI() throws Exception { return ParticipationFactory.applySGIonFeedback(receivedFileUploadExercise); } - @Order(1) @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testSubmitFileUploadAssessment_asInstructor() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseIntegrationTest.java index 1b1a261a7e19..58c5c5a4a3b2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileupload/FileUploadExerciseIntegrationTest.java @@ -3,7 +3,9 @@ import static de.tum.in.www1.artemis.util.TestResourceUtils.HalfSecond; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -27,6 +29,7 @@ import org.springframework.security.test.context.support.WithMockUser; import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.Feedback; @@ -35,6 +38,7 @@ import de.tum.in.www1.artemis.domain.GradingCriterion; import de.tum.in.www1.artemis.domain.GradingInstruction; import de.tum.in.www1.artemis.domain.Result; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.IncludedInOverallScore; import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; import de.tum.in.www1.artemis.domain.metis.conversation.Channel; @@ -85,6 +89,15 @@ class FileUploadExerciseIntegrationTest extends AbstractSpringIntegrationIndepen @Autowired private PageableSearchUtilService pageableSearchUtilService; + @Autowired + private CompetencyUtilService competencyUtilService; + + private FileUploadExercise fileUploadExercise; + + private Course course; + + private Competency competency; + private Set gradingCriteria; private final String creationFilePattern = "png, pdf, jPg , r, DOCX"; @@ -92,13 +105,16 @@ class FileUploadExerciseIntegrationTest extends AbstractSpringIntegrationIndepen @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 2, 1, 1, 1); + + fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); + course = fileUploadExercise.getCourseViaExerciseGroupOrCourseMember(); + competency = competencyUtilService.createCompetency(course); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFileUploadExerciseFails() throws Exception { String filePattern = "Example file pattern"; - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); fileUploadExercise.setFilePattern(filePattern); request.postWithResponseBody("/api/file-upload-exercises", fileUploadExercise, FileUploadExercise.class, HttpStatus.BAD_REQUEST); } @@ -107,7 +123,6 @@ void createFileUploadExerciseFails() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFileUploadExerciseFailsIfAlreadyCreated() throws Exception { String filePattern = "Example file pattern"; - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); fileUploadExercise.setFilePattern(filePattern); fileUploadExercise = fileUploadExerciseRepository.save(fileUploadExercise); request.postWithResponseBody("/api/file-upload-exercises", fileUploadExercise, FileUploadExercise.class, HttpStatus.BAD_REQUEST); @@ -116,7 +131,6 @@ void createFileUploadExerciseFailsIfAlreadyCreated() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFileUploadExercise_InvalidMaxScore() throws Exception { - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); fileUploadExercise.setFilePattern(creationFilePattern); fileUploadExercise.setMaxPoints(0.0); request.postWithResponseBody("/api/file-upload-exercises", fileUploadExercise, FileUploadExercise.class, HttpStatus.BAD_REQUEST); @@ -125,9 +139,7 @@ void createFileUploadExercise_InvalidMaxScore() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFileUploadExercise_InvalidInstructor() throws Exception { - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); // make sure the instructor is not instructor for this course anymore by changing the courses' instructor group name - var course = fileUploadExercise.getCourseViaExerciseGroupOrCourseMember(); course.setInstructorGroupName("new-instructor-group-name"); courseRepository.save(course); fileUploadExercise.setFilePattern(creationFilePattern); @@ -138,7 +150,6 @@ void createFileUploadExercise_InvalidInstructor() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFileUploadExerciseFails_AlmostEmptyFilePattern() throws Exception { - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); fileUploadExercise.setFilePattern(" "); gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(fileUploadExercise); request.postWithResponseBody("/api/file-upload-exercises", fileUploadExercise, FileUploadExercise.class, HttpStatus.BAD_REQUEST); @@ -147,7 +158,6 @@ void createFileUploadExerciseFails_AlmostEmptyFilePattern() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFileUploadExerciseFails_EmptyFilePattern() throws Exception { - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); fileUploadExercise.setFilePattern(""); gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(fileUploadExercise); request.postWithResponseBody("/api/file-upload-exercises", fileUploadExercise, FileUploadExercise.class, HttpStatus.BAD_REQUEST); @@ -156,7 +166,6 @@ void createFileUploadExerciseFails_EmptyFilePattern() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFileUploadExercise_IncludedAsBonusInvalidBonusPoints() throws Exception { - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); fileUploadExercise.setFilePattern(creationFilePattern); fileUploadExercise.setMaxPoints(10.0); fileUploadExercise.setBonusPoints(1.0); @@ -167,7 +176,6 @@ void createFileUploadExercise_IncludedAsBonusInvalidBonusPoints() throws Excepti @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFileUploadExercise_NotIncludedInvalidBonusPoints() throws Exception { - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); fileUploadExercise.setFilePattern(creationFilePattern); fileUploadExercise.setMaxPoints(10.0); fileUploadExercise.setBonusPoints(1.0); @@ -180,8 +188,7 @@ void createFileUploadExercise_NotIncludedInvalidBonusPoints() throws Exception { @ValueSource(strings = { "exercise-new-fileupload-exerci", "" }) @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFileUploadExercise(String channelName) throws Exception { - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); - courseUtilService.enableMessagingForCourse(fileUploadExercise.getCourseViaExerciseGroupOrCourseMember()); + courseUtilService.enableMessagingForCourse(course); fileUploadExercise.setFilePattern(creationFilePattern); fileUploadExercise.setTitle("new fileupload exercise"); fileUploadExercise.setChannelName(channelName); @@ -196,8 +203,7 @@ void createFileUploadExercise(String channelName) throws Exception { assertThat(receivedFileUploadExercise.getFilePattern()).isEqualTo(creationFilePattern.toLowerCase().replaceAll("\\s+", "")); assertThat(receivedFileUploadExercise.getCourseViaExerciseGroupOrCourseMember()).as("course was set for normal exercise").isNotNull(); assertThat(receivedFileUploadExercise.getExerciseGroup()).as("exerciseGroup was not set for normal exercise").isNull(); - assertThat(receivedFileUploadExercise.getCourseViaExerciseGroupOrCourseMember().getId()).as("exerciseGroupId was set correctly") - .isEqualTo(fileUploadExercise.getCourseViaExerciseGroupOrCourseMember().getId()); + assertThat(receivedFileUploadExercise.getCourseViaExerciseGroupOrCourseMember().getId()).as("exerciseGroupId was set correctly").isEqualTo(course.getId()); GradingCriterion criterionWithoutTitle = GradingCriterionUtil.findGradingCriterionByTitle(receivedFileUploadExercise, null); assertThat(criterionWithoutTitle.getStructuredGradingInstructions()).hasSize(1); @@ -248,7 +254,7 @@ void createFileUploadExerciseForExam_invalidExercise_dates(InvalidExamExerciseDa void createFileUploadExercise_setBothCourseAndExerciseGroupOrNeither_badRequest() throws Exception { ExerciseGroup exerciseGroup = examUtilService.addExerciseGroupWithExamAndCourse(true); FileUploadExercise fileUploadExercise = FileUploadExerciseFactory.generateFileUploadExerciseForExam(creationFilePattern, exerciseGroup); - fileUploadExercise.setCourse(fileUploadExercise.getExerciseGroup().getExam().getCourse()); + fileUploadExercise.setCourse(fileUploadExercise.getCourseViaExerciseGroupOrCourseMember()); request.postWithResponseBody("/api/file-upload-exercises", fileUploadExercise, FileUploadExercise.class, HttpStatus.BAD_REQUEST); @@ -358,6 +364,16 @@ void testDeleteFileUploadExerciseWithChannel() throws Exception { assertThat(exerciseChannelAfterDelete).isEmpty(); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteFileUploadExerciseWithCompetency() throws Exception { + fileUploadExercise.setCompetencies(Set.of(competency)); + fileUploadExercise = fileUploadExerciseRepository.save(fileUploadExercise); + request.delete("/api/file-upload-exercises/" + fileUploadExercise.getId(), HttpStatus.OK); + + verify(competencyProgressService).updateProgressByCompetencyAsync(eq(competency)); + } + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void deleteFileUploadExercise_asStudent() throws Exception { @@ -404,6 +420,7 @@ void updateFileUploadExercise_asInstructor() throws Exception { final ZonedDateTime dueDate = ZonedDateTime.now().plusDays(10); fileUploadExercise.setDueDate(dueDate); fileUploadExercise.setAssessmentDueDate(ZonedDateTime.now().plusDays(11)); + fileUploadExercise.setCompetencies(Set.of(competency)); FileUploadExercise receivedFileUploadExercise = request.putWithResponseBody("/api/file-upload-exercises/" + fileUploadExercise.getId() + "?notificationText=notification", fileUploadExercise, FileUploadExercise.class, HttpStatus.OK); @@ -413,6 +430,7 @@ void updateFileUploadExercise_asInstructor() throws Exception { assertThat(receivedFileUploadExercise.getCourseViaExerciseGroupOrCourseMember().getId()).as("courseId was not updated").isEqualTo(course.getId()); verify(examLiveEventsService, never()).createAndSendProblemStatementUpdateEvent(any(), any(), any()); verify(groupNotificationScheduleService, times(1)).checkAndCreateAppropriateNotificationsWhenUpdatingExercise(any(), any(), any()); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(fileUploadExercise), eq(Optional.of(fileUploadExercise))); } @Test @@ -460,7 +478,7 @@ void updateFileUploadExerciseForExam_invalid_dates(InvalidExamExerciseDateConfig @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateFileUploadExercise_setBothCourseAndExerciseGroupOrNeither_badRequest() throws Exception { FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.addCourseExamExerciseGroupWithOneFileUploadExercise(); - fileUploadExercise.setCourse(fileUploadExercise.getExerciseGroup().getExam().getCourse()); + fileUploadExercise.setCourse(fileUploadExercise.getCourseViaExerciseGroupOrCourseMember()); request.putWithResponseBody("/api/file-upload-exercises/" + fileUploadExercise.getId(), fileUploadExercise, FileUploadExercise.class, HttpStatus.BAD_REQUEST); @@ -473,17 +491,15 @@ void updateFileUploadExercise_setBothCourseAndExerciseGroupOrNeither_badRequest( @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateFileUploadExercise_conversionBetweenCourseAndExamExercise_badRequest() throws Exception { - FileUploadExercise fileUploadExerciseWithCourse = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); FileUploadExercise fileUploadExerciseWithExerciseGroup = fileUploadExerciseUtilService.addCourseExamExerciseGroupWithOneFileUploadExercise(); - fileUploadExerciseWithCourse.setCourse(null); - fileUploadExerciseWithCourse.setExerciseGroup(fileUploadExerciseWithExerciseGroup.getExerciseGroup()); + fileUploadExercise.setCourse(null); + fileUploadExercise.setExerciseGroup(fileUploadExerciseWithExerciseGroup.getExerciseGroup()); - fileUploadExerciseWithExerciseGroup.setCourse(fileUploadExerciseWithCourse.getCourseViaExerciseGroupOrCourseMember()); + fileUploadExerciseWithExerciseGroup.setCourse(course); fileUploadExerciseWithExerciseGroup.setExerciseGroup(null); - request.putWithResponseBody("/api/file-upload-exercises/" + fileUploadExerciseWithCourse.getId(), fileUploadExerciseWithCourse, FileUploadExercise.class, - HttpStatus.BAD_REQUEST); + request.putWithResponseBody("/api/file-upload-exercises/" + fileUploadExercise.getId(), fileUploadExercise, FileUploadExercise.class, HttpStatus.BAD_REQUEST); request.putWithResponseBody("/api/file-upload-exercises/" + fileUploadExerciseWithExerciseGroup.getId(), fileUploadExerciseWithExerciseGroup, FileUploadExercise.class, HttpStatus.BAD_REQUEST); } @@ -491,7 +507,6 @@ void updateFileUploadExercise_conversionBetweenCourseAndExamExercise_badRequest( @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateModelingExerciseDueDate() throws Exception { - FileUploadExercise fileUploadExercise = fileUploadExerciseUtilService.createFileUploadExercisesWithCourse().getFirst(); fileUploadExercise = fileUploadExerciseRepository.save(fileUploadExercise); final ZonedDateTime individualDueDate = ZonedDateTime.now().plusHours(20); @@ -705,15 +720,17 @@ void testImportFileUploadExerciseFromCourseToCourseAsEditorSuccess() throws Exce expectedFileUploadExercise.setCourse(course2); String uniqueChannelName = "test" + UUID.randomUUID().toString().substring(0, 8); expectedFileUploadExercise.setChannelName(uniqueChannelName); + expectedFileUploadExercise.setCompetencies(Set.of(competency)); + var sourceExerciseId = expectedFileUploadExercise.getId(); var importedFileUploadExercise = request.postWithResponseBody("/api/file-upload-exercises/import/" + sourceExerciseId, expectedFileUploadExercise, FileUploadExercise.class, HttpStatus.CREATED); - assertThat(importedFileUploadExercise).usingRecursiveComparison() - .ignoringFields("id", "course", "shortName", "releaseDate", "dueDate", "assessmentDueDate", "exampleSolutionPublicationDate", "channelNameTransient") - .isEqualTo(expectedFileUploadExercise); + assertThat(importedFileUploadExercise).usingRecursiveComparison().ignoringFields("id", "course", "shortName", "releaseDate", "dueDate", "assessmentDueDate", + "exampleSolutionPublicationDate", "channelNameTransient", "competencies").isEqualTo(expectedFileUploadExercise); Channel channelFromDB = channelRepository.findChannelByExerciseId(importedFileUploadExercise.getId()); assertThat(channelFromDB).isNotNull(); assertThat(channelFromDB.getName()).isEqualTo(uniqueChannelName); + verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(importedFileUploadExercise)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseIntegrationTest.java index 2813feed61c4..c84e0840a497 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modeling/ModelingExerciseIntegrationTest.java @@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -29,6 +30,7 @@ import org.springframework.util.LinkedMultiValueMap; import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ExampleSubmission; import de.tum.in.www1.artemis.domain.Feedback; @@ -37,6 +39,7 @@ import de.tum.in.www1.artemis.domain.Result; import de.tum.in.www1.artemis.domain.Team; import de.tum.in.www1.artemis.domain.TeamAssignmentConfig; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.DiagramType; import de.tum.in.www1.artemis.domain.enumeration.DifficultyLevel; @@ -112,10 +115,15 @@ class ModelingExerciseIntegrationTest extends AbstractSpringIntegrationLocalCILo @Autowired private PageableSearchUtilService pageableSearchUtilService; + @Autowired + private CompetencyUtilService competencyUtilService; + private ModelingExercise classExercise; private Set gradingCriteria; + private Competency competency; + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 2, 1, 0, 1); @@ -126,6 +134,7 @@ void initTestCase() { userUtilService.createAndSaveUser(TEST_PREFIX + "instructor2"); userUtilService.createAndSaveUser(TEST_PREFIX + "tutor2"); + competency = competencyUtilService.createCompetency(course); } @Test @@ -215,6 +224,7 @@ void testUpdateModelingExercise_asInstructor() throws Exception { ModelingExercise modelingExercise = ModelingExerciseFactory.createModelingExercise(classExercise.getCourseViaExerciseGroupOrCourseMember().getId()); courseUtilService.enableMessagingForCourse(modelingExercise.getCourseViaExerciseGroupOrCourseMember()); modelingExercise.setChannelName("testchannel-" + UUID.randomUUID().toString().substring(0, 8)); + modelingExercise.setCompetencies(Set.of(competency)); ModelingExercise createdModelingExercise = request.postWithResponseBody("/api/modeling-exercises", modelingExercise, ModelingExercise.class, HttpStatus.CREATED); gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(modelingExercise); @@ -228,9 +238,15 @@ void testUpdateModelingExercise_asInstructor() throws Exception { assertThat(returnedModelingExercise.getGradingCriteria()).hasSameSizeAs(gradingCriteria); verify(groupNotificationService).notifyStudentAndEditorAndInstructorGroupAboutExerciseUpdate(returnedModelingExercise, notificationText); verify(examLiveEventsService, never()).createAndSendProblemStatementUpdateEvent(eq(returnedModelingExercise), eq(notificationText), any()); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(createdModelingExercise), + eq(Optional.of(createdModelingExercise))); + } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testUpdateModelingExerciseWrongCourseId_asInstructor() throws Exception { // use an arbitrary course id that was not yet stored on the server to get a bad request in the PUT call - modelingExercise = ModelingExerciseFactory.createModelingExercise(Long.MAX_VALUE, classExercise.getId()); + ModelingExercise modelingExercise = ModelingExerciseFactory.createModelingExercise(Long.MAX_VALUE, classExercise.getId()); request.put("/api/modeling-exercises", modelingExercise, HttpStatus.CONFLICT); } @@ -387,6 +403,17 @@ void testDeleteModelingExercise_asInstructor() throws Exception { request.delete("/api/modeling-exercises/" + classExercise.getId(), HttpStatus.NOT_FOUND); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteModelingExerciseWithCompetency() throws Exception { + classExercise.setCompetencies(Set.of(competency)); + modelingExerciseRepository.save(classExercise); + + request.delete("/api/modeling-exercises/" + classExercise.getId(), HttpStatus.OK); + + verify(competencyProgressService).updateProgressByCompetencyAsync(eq(competency)); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testDeleteModelingExerciseWithChannel() throws Exception { @@ -445,14 +472,16 @@ void importModelingExerciseFromCourseToCourse() throws Exception { modelingExerciseToImport.setCourse(course2); String uniqueChannelName = "channel-" + UUID.randomUUID().toString().substring(0, 8); modelingExerciseToImport.setChannelName(uniqueChannelName); + modelingExerciseToImport.setCompetencies(Set.of(competency)); var importedExercise = request.postWithResponseBody("/api/modeling-exercises/import/" + modelingExerciseToImport.getId(), modelingExerciseToImport, ModelingExercise.class, HttpStatus.CREATED); assertThat(importedExercise).usingRecursiveComparison().ignoringFields("id", "course", "shortName", "releaseDate", "dueDate", "assessmentDueDate", - "exampleSolutionPublicationDate", "channelNameTransient", "plagiarismDetectionConfig.id").isEqualTo(modelingExerciseToImport); + "exampleSolutionPublicationDate", "channelNameTransient", "plagiarismDetectionConfig.id", "competencies").isEqualTo(modelingExerciseToImport); Channel channelFromDB = channelRepository.findChannelByExerciseId(importedExercise.getId()); assertThat(channelFromDB).isNotNull(); assertThat(channelFromDB.getName()).isEqualTo(uniqueChannelName); + verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(importedExercise)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java index b6af6ce834e9..2004f5738e23 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java @@ -4,11 +4,16 @@ import static de.tum.in.www1.artemis.config.Constants.LOCALCI_WORKING_DIRECTORY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import java.io.IOException; import java.time.ZonedDateTime; import java.util.Map; +import java.util.Optional; +import java.util.Set; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; @@ -26,9 +31,11 @@ import org.springframework.util.LinkedMultiValueMap; import de.tum.in.www1.artemis.AbstractSpringIntegrationLocalCILocalVCTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.connector.AeolusRequestMockProvider; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.AeolusTarget; import de.tum.in.www1.artemis.domain.enumeration.ProjectType; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; @@ -53,6 +60,9 @@ class ProgrammingExerciseLocalVCLocalCIIntegrationTest extends AbstractSpringInt @Autowired private AeolusRequestMockProvider aeolusRequestMockProvider; + @Autowired + private CompetencyUtilService competencyUtilService; + private Course course; private ProgrammingExercise programmingExercise; @@ -65,6 +75,8 @@ class ProgrammingExerciseLocalVCLocalCIIntegrationTest extends AbstractSpringInt private LocalRepository assignmentRepository; + private Competency competency; + @Value("${artemis.user-management.internal-admin.username}") private String localVCUsername; @@ -120,6 +132,8 @@ void setup() throws Exception { // Check that the repository folders were created in the file system for all base repositories. localVCLocalCITestService.verifyRepositoryFoldersExist(programmingExercise, localVCBasePath); + + competency = competencyUtilService.createCompetency(course); } @AfterEach @@ -138,6 +152,7 @@ void testCreateProgrammingExercise() throws Exception { ProgrammingExercise newExercise = ProgrammingExerciseFactory.generateProgrammingExercise(ZonedDateTime.now().minusDays(1), ZonedDateTime.now().plusDays(7), course); newExercise.setProjectType(ProjectType.PLAIN_GRADLE); + newExercise.setCompetencies(Set.of(competency)); // Mock dockerClient.copyArchiveFromContainerCmd() such that it returns a dummy commitHash for both the assignment and the test repository. // Note: The stub needs to receive the same object twice because there are two requests to the same method (one for the template participation and one for the solution @@ -168,16 +183,20 @@ void testCreateProgrammingExercise() throws Exception { // Also check that the template and solution repositories were built successfully. localVCLocalCITestService.testLatestSubmission(createdExercise.getTemplateParticipation().getId(), null, 0, false); localVCLocalCITestService.testLatestSubmission(createdExercise.getSolutionParticipation().getId(), null, 13, false); + + verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(createdExercise)); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testUpdateProgrammingExercise() throws Exception { programmingExercise.setReleaseDate(ZonedDateTime.now().plusHours(1)); + programmingExercise.setCompetencies(Set.of(competency)); ProgrammingExercise updatedExercise = request.putWithResponseBody("/api/programming-exercises", programmingExercise, ProgrammingExercise.class, HttpStatus.OK); assertThat(updatedExercise.getReleaseDate()).isEqualTo(programmingExercise.getReleaseDate()); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(programmingExercise), eq(Optional.of(programmingExercise))); } @Test @@ -194,6 +213,9 @@ void testUpdateProgrammingExercise_templateRepositoryUriIsInvalid() throws Excep @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testDeleteProgrammingExercise() throws Exception { + programmingExercise.setCompetencies(Set.of(competency)); + programmingExerciseRepository.save(programmingExercise); + // Delete the exercise var params = new LinkedMultiValueMap(); params.add("deleteStudentReposBuildPlans", "true"); @@ -207,6 +229,7 @@ void testDeleteProgrammingExercise() throws Exception { assertThat(solutionRepositoryUri.getLocalRepositoryPath(localVCBasePath)).doesNotExist(); LocalVCRepositoryUri testsRepositoryUri = new LocalVCRepositoryUri(programmingExercise.getTestRepositoryUri()); assertThat(testsRepositoryUri.getLocalRepositoryPath(localVCBasePath)).doesNotExist(); + verify(competencyProgressService).updateProgressByCompetencyAsync(eq(competency)); } @Test @@ -241,6 +264,7 @@ void testImportProgrammingExercise() throws Exception { var params = new LinkedMultiValueMap(); params.add("recreateBuildPlans", "true"); exerciseToBeImported.setChannelName("testchannel-pe-imported"); + exerciseToBeImported.setCompetencies(Set.of(competency)); var importedExercise = request.postWithResponseBody("/api/programming-exercises/import/" + programmingExercise.getId(), exerciseToBeImported, ProgrammingExercise.class, params, HttpStatus.OK); @@ -255,6 +279,7 @@ void testImportProgrammingExercise() throws Exception { .orElseThrow(); localVCLocalCITestService.testLatestSubmission(templateParticipation.getId(), null, 0, false); localVCLocalCITestService.testLatestSubmission(solutionParticipation.getId(), null, 13, false); + verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(importedExercise)); } @Nested diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitIntegrationTest.java index b06a7bda9393..2fd4b645a516 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/AttachmentUnitIntegrationTest.java @@ -2,12 +2,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.Optional; +import java.util.Set; import jakarta.validation.constraints.NotNull; @@ -31,8 +36,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.domain.Attachment; import de.tum.in.www1.artemis.domain.Lecture; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.Slide; @@ -63,12 +70,17 @@ class AttachmentUnitIntegrationTest extends AbstractSpringIntegrationIndependent @Autowired private LectureUtilService lectureUtilService; + @Autowired + private CompetencyUtilService competencyUtilService; + private Lecture lecture1; private Attachment attachment; private AttachmentUnit attachmentUnit; + private Competency competency; + @Autowired private ObjectMapper mapper; @@ -86,6 +98,8 @@ void initTestCase() { userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); userUtilService.createAndSaveUser(TEST_PREFIX + "tutor42"); userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + + competency = competencyUtilService.createCompetency(lecture1.getCourse()); } private void testAllPreAuthorize() throws Exception { @@ -203,6 +217,7 @@ void updateLectureAttachmentUnitWithSameFileName() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createAttachmentUnit_asInstructor_shouldCreateAttachmentUnit() throws Exception { + attachmentUnit.setCompetencies(Set.of(competency)); var result = request.performMvcRequest(buildCreateAttachmentUnit(attachmentUnit, attachment)).andExpect(status().isCreated()).andReturn(); var persistedAttachmentUnit = mapper.readValue(result.getResponse().getContentAsString(), AttachmentUnit.class); assertThat(persistedAttachmentUnit.getId()).isNotNull(); @@ -213,6 +228,7 @@ void createAttachmentUnit_asInstructor_shouldCreateAttachmentUnit() throws Excep await().untilAsserted(() -> assertThat(slideRepository.findAllByAttachmentUnitId(persistedAttachmentUnit.getId())).hasSize(SLIDE_COUNT)); assertThat(updatedAttachmentUnit.getAttachment()).isEqualTo(persistedAttachment); assertThat(updatedAttachmentUnit.getAttachment().getName()).isEqualTo("LoremIpsum"); + verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(updatedAttachmentUnit)); } @Test @@ -238,6 +254,7 @@ void createAttachmentUnit_withAttachmentId_shouldReturnBadRequest() throws Excep @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateAttachmentUnit_asInstructor_shouldUpdateAttachmentUnit() throws Exception { + attachmentUnit.setCompetencies(Set.of(competency)); var createResult = request.performMvcRequest(buildCreateAttachmentUnit(attachmentUnit, attachment)).andExpect(status().isCreated()).andReturn(); var attachmentUnit = mapper.readValue(createResult.getResponse().getContentAsString(), AttachmentUnit.class); var attachment = attachmentUnit.getAttachment(); @@ -257,6 +274,7 @@ void updateAttachmentUnit_asInstructor_shouldUpdateAttachmentUnit() throws Excep attachment = attachmentRepository.findById(attachment.getId()).orElseThrow(); assertThat(attachmentUnit2.getAttachment()).isEqualTo(attachment); assertThat(attachment.getAttachmentUnit()).isEqualTo(attachmentUnit2); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(attachmentUnit), eq(Optional.of(attachmentUnit))); } @Test @@ -325,11 +343,13 @@ void getAttachmentUnit_correctId_shouldReturnAttachmentUnit() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void deleteAttachmentUnit_withAttachment_shouldDeleteAttachment() throws Exception { + attachmentUnit.setCompetencies(Set.of(competency)); var result = request.performMvcRequest(buildCreateAttachmentUnit(attachmentUnit, attachment)).andExpect(status().isCreated()).andReturn(); var persistedAttachmentUnit = mapper.readValue(result.getResponse().getContentAsString(), AttachmentUnit.class); assertThat(persistedAttachmentUnit.getId()).isNotNull(); assertThat(slideRepository.findAllByAttachmentUnitId(persistedAttachmentUnit.getId())).hasSize(0); request.delete("/api/lectures/" + lecture1.getId() + "/lecture-units/" + persistedAttachmentUnit.getId(), HttpStatus.OK); request.get("/api/lectures/" + lecture1.getId() + "/attachment-units/" + persistedAttachmentUnit.getId(), HttpStatus.NOT_FOUND, AttachmentUnit.class); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(persistedAttachmentUnit), eq(Optional.empty())); } } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java index c2497998f44a..72cb78321c40 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/ExerciseUnitIntegrationTest.java @@ -2,6 +2,9 @@ import static de.tum.in.www1.artemis.util.RequestUtilService.deleteProgrammingExerciseParamsFalse; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import java.util.HashSet; import java.util.List; @@ -211,6 +214,7 @@ void deleteExerciseUnit_exerciseConnectedWithExerciseUnit_shouldNOTDeleteExercis request.get("/api/exercises/" + exercise.getId(), HttpStatus.OK, Exercise.class); } + verify(competencyProgressService, never()).updateProgressForUpdatedLearningObjectAsync(any(), any()); } } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java index 06c717c19416..0da05efa33e3 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/LectureIntegrationTest.java @@ -1,6 +1,10 @@ package de.tum.in.www1.artemis.lecture; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import java.time.ZonedDateTime; import java.util.List; @@ -18,10 +22,12 @@ import org.springframework.security.test.context.support.WithMockUser; import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.domain.Attachment; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Lecture; import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.lecture.AttachmentUnit; import de.tum.in.www1.artemis.domain.lecture.ExerciseUnit; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; @@ -32,6 +38,7 @@ import de.tum.in.www1.artemis.post.ConversationUtilService; import de.tum.in.www1.artemis.repository.AttachmentRepository; import de.tum.in.www1.artemis.repository.LectureRepository; +import de.tum.in.www1.artemis.repository.LectureUnitRepository; import de.tum.in.www1.artemis.repository.TextExerciseRepository; import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository; import de.tum.in.www1.artemis.util.PageableSearchUtilService; @@ -58,6 +65,15 @@ class LectureIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Autowired private PageableSearchUtilService pageableSearchUtilService; + @Autowired + private ConversationUtilService conversationUtilService; + + @Autowired + private CompetencyUtilService competencyUtilService; + + @Autowired + private LectureUnitRepository lectureUnitRepository; + private Attachment attachmentDirectOfLecture; private Attachment attachmentOfAttachmentUnit; @@ -68,8 +84,9 @@ class LectureIntegrationTest extends AbstractSpringIntegrationIndependentTest { private Lecture lecture1; - @Autowired - private ConversationUtilService conversationUtilService; + private AttachmentUnit attachmentUnit; + + private Competency competency; @BeforeEach void initTestCase() throws Exception { @@ -86,24 +103,26 @@ void initTestCase() throws Exception { channel.setIsArchived(false); channel.setName("lecture-channel"); lecture.setTitle("Lecture " + lecture.getId()); // needed for search by title - this.lecture1 = lectureRepository.save(lecture); + lecture1 = lectureRepository.save(lecture); channel.setLecture(this.lecture1); channelRepository.save(channel); - this.textExercise = textExerciseRepository.findByCourseIdWithCategories(course1.getId()).stream().findFirst().orElseThrow(); + textExercise = textExerciseRepository.findByCourseIdWithCategories(course1.getId()).stream().findFirst().orElseThrow(); // Add users that are not in the course userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); // Setting up a lecture with various kinds of content ExerciseUnit exerciseUnit = lectureUtilService.createExerciseUnit(textExercise); - AttachmentUnit attachmentUnit = lectureUtilService.createAttachmentUnit(true); - this.attachmentOfAttachmentUnit = attachmentUnit.getAttachment(); + attachmentUnit = lectureUtilService.createAttachmentUnit(true); + attachmentOfAttachmentUnit = attachmentUnit.getAttachment(); VideoUnit videoUnit = lectureUtilService.createVideoUnit(); TextUnit textUnit = lectureUtilService.createTextUnit(); OnlineUnit onlineUnit = lectureUtilService.createOnlineUnit(); addAttachmentToLecture(); - this.lecture1 = lectureUtilService.addLectureUnitsToLecture(this.lecture1, List.of(exerciseUnit, attachmentUnit, videoUnit, textUnit, onlineUnit)); + lecture1 = lectureUtilService.addLectureUnitsToLecture(this.lecture1, List.of(exerciseUnit, attachmentUnit, videoUnit, textUnit, onlineUnit)); + + competency = competencyUtilService.createCompetency(course1); } private void addAttachmentToLecture() { @@ -337,9 +356,15 @@ void getLecture_AttachmentNotReleased_shouldGetLectureWithoutAttachmentUnitAndAt @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void deleteLecture_lectureExists_shouldDeleteLecture() throws Exception { + attachmentUnit.setCompetencies(Set.of(competency)); + lectureUnitRepository.save(attachmentUnit); + request.delete("/api/lectures/" + lecture1.getId(), HttpStatus.OK); Optional lectureOptional = lectureRepository.findById(lecture1.getId()); assertThat(lectureOptional).isEmpty(); + + // ExerciseUnits do not have competencies, their exercises do + verify(competencyProgressService, timeout(1000).times(lecture1.getLectureUnits().size() - 1)).updateProgressForUpdatedLearningObjectAsync(any(), eq(Optional.empty())); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/OnlineUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/OnlineUnitIntegrationTest.java index 8c5050193662..74f7e9ebb1ee 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/OnlineUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/OnlineUnitIntegrationTest.java @@ -1,13 +1,18 @@ package de.tum.in.www1.artemis.lecture; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.net.URI; import java.util.List; +import java.util.Optional; +import java.util.Set; import org.jsoup.Connection; import org.jsoup.Jsoup; @@ -24,7 +29,9 @@ import org.springframework.util.LinkedMultiValueMap; import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.domain.Lecture; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.OnlineUnit; import de.tum.in.www1.artemis.repository.LectureRepository; @@ -44,10 +51,15 @@ class OnlineUnitIntegrationTest extends AbstractSpringIntegrationIndependentTest @Autowired private LectureUtilService lectureUtilService; + @Autowired + private CompetencyUtilService competencyUtilService; + private Lecture lecture1; private OnlineUnit onlineUnit; + private Competency competency; + private MockedStatic jsoupMock; @BeforeEach @@ -64,6 +76,8 @@ void initTestCase() { userUtilService.createAndSaveUser(TEST_PREFIX + "tutor42"); userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + competency = competencyUtilService.createCompetency(lecture1.getCourse()); + jsoupMock = mockStatic(Jsoup.class); } @@ -93,9 +107,11 @@ void testAll_asStudent() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createOnlineUnit_asInstructor_shouldCreateOnlineUnit() throws Exception { + onlineUnit.setCompetencies(Set.of(competency)); onlineUnit.setSource("https://www.youtube.com/embed/8iU8LPEa4o0"); var persistedOnlineUnit = request.postWithResponseBody("/api/lectures/" + this.lecture1.getId() + "/online-units", onlineUnit, OnlineUnit.class, HttpStatus.CREATED); assertThat(persistedOnlineUnit.getId()).isNotNull(); + verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(persistedOnlineUnit)); } @Test @@ -122,6 +138,7 @@ void createOnlineUnit_withId_shouldReturnBadRequest() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateOnlineUnit_asInstructor_shouldUpdateOnlineUnit() throws Exception { + onlineUnit.setCompetencies(Set.of(competency)); persistOnlineUnitWithLecture(); this.onlineUnit = (OnlineUnit) lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lecture1.getId()).getLectureUnits().stream().findFirst().orElseThrow(); @@ -129,6 +146,7 @@ void updateOnlineUnit_asInstructor_shouldUpdateOnlineUnit() throws Exception { this.onlineUnit.setDescription("Changed"); this.onlineUnit = request.putWithResponseBody("/api/lectures/" + lecture1.getId() + "/online-units", this.onlineUnit, OnlineUnit.class, HttpStatus.OK); assertThat(this.onlineUnit.getDescription()).isEqualTo("Changed"); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(onlineUnit), eq(Optional.of(onlineUnit))); } @Test @@ -238,12 +256,14 @@ void getOnlineResource_malformedUrl(String link) throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void deleteOnlineUnit_correctId_shouldDeleteOnlineUnit() throws Exception { + onlineUnit.setCompetencies(Set.of(competency)); persistOnlineUnitWithLecture(); this.onlineUnit = (OnlineUnit) lectureRepository.findByIdWithLectureUnitsAndAttachmentsElseThrow(lecture1.getId()).getLectureUnits().stream().findFirst().orElseThrow(); assertThat(this.onlineUnit.getId()).isNotNull(); request.delete("/api/lectures/" + lecture1.getId() + "/lecture-units/" + this.onlineUnit.getId(), HttpStatus.OK); request.get("/api/lectures/" + lecture1.getId() + "/online-units/" + this.onlineUnit.getId(), HttpStatus.NOT_FOUND, OnlineUnit.class); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(onlineUnit), eq(Optional.empty())); } } diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/TextUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/TextUnitIntegrationTest.java index f80071fd82b0..e66c2661c579 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/TextUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/TextUnitIntegrationTest.java @@ -1,8 +1,13 @@ package de.tum.in.www1.artemis.lecture; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import java.util.List; +import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,7 +16,9 @@ import org.springframework.security.test.context.support.WithMockUser; import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.domain.Lecture; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.TextUnit; import de.tum.in.www1.artemis.repository.LectureRepository; @@ -30,10 +37,15 @@ class TextUnitIntegrationTest extends AbstractSpringIntegrationIndependentTest { @Autowired private LectureUtilService lectureUtilService; + @Autowired + private CompetencyUtilService competencyUtilService; + private Lecture lecture; private TextUnit textUnit; + private Competency competency; + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 1, 1, 1, 1); @@ -47,6 +59,8 @@ void initTestCase() { userUtilService.createAndSaveUser(TEST_PREFIX + "tutor42"); userUtilService.createAndSaveUser(TEST_PREFIX + "editor42"); userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + + competency = competencyUtilService.createCompetency(lecture.getCourse()); } private void testAllPreAuthorize() throws Exception { @@ -70,9 +84,11 @@ void testAll_asStudent() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void createTextUnit_asEditor_shouldCreateTextUnitUnit() throws Exception { + textUnit.setCompetencies(Set.of(competency)); var persistedTextUnit = request.postWithResponseBody("/api/lectures/" + this.lecture.getId() + "/text-units", textUnit, TextUnit.class, HttpStatus.CREATED); assertThat(persistedTextUnit.getId()).isNotNull(); assertThat(persistedTextUnit.getName()).isEqualTo("LoremIpsum"); + verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(persistedTextUnit)); } @Test @@ -89,11 +105,13 @@ void createTextUnit_EditorNotInCourse_shouldReturnForbidden() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void updateTextUnit_asEditor_shouldUpdateTextUnit() throws Exception { + textUnit.setCompetencies(Set.of(competency)); persistTextUnitWithLecture(); TextUnit textUnitFromRequest = request.get("/api/lectures/" + lecture.getId() + "/text-units/" + this.textUnit.getId(), HttpStatus.OK, TextUnit.class); textUnitFromRequest.setContent("Changed"); TextUnit updatedTextUnit = request.putWithResponseBody("/api/lectures/" + lecture.getId() + "/text-units", textUnitFromRequest, TextUnit.class, HttpStatus.OK); assertThat(updatedTextUnit.getContent()).isEqualTo("Changed"); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(textUnit), eq(Optional.of(textUnit))); } @Test @@ -137,10 +155,12 @@ void getTextUnit_correctId_shouldReturnTextUnit() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void deleteTextUnit_correctId_shouldDeleteTextUnit() throws Exception { + textUnit.setCompetencies(Set.of(competency)); persistTextUnitWithLecture(); assertThat(this.textUnit.getId()).isNotNull(); request.delete("/api/lectures/" + lecture.getId() + "/lecture-units/" + this.textUnit.getId(), HttpStatus.OK); request.get("/api/lectures/" + lecture.getId() + "/text-units/" + this.textUnit.getId(), HttpStatus.NOT_FOUND, TextUnit.class); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(textUnit), eq(Optional.empty())); } private void persistTextUnitWithLecture() { diff --git a/src/test/java/de/tum/in/www1/artemis/lecture/VideoUnitIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/lecture/VideoUnitIntegrationTest.java index 2325f86cfb52..4ac391743fb8 100644 --- a/src/test/java/de/tum/in/www1/artemis/lecture/VideoUnitIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/lecture/VideoUnitIntegrationTest.java @@ -1,8 +1,13 @@ package de.tum.in.www1.artemis.lecture; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; import java.util.List; +import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,7 +16,9 @@ import org.springframework.security.test.context.support.WithMockUser; import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.domain.Lecture; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.lecture.LectureUnit; import de.tum.in.www1.artemis.domain.lecture.VideoUnit; import de.tum.in.www1.artemis.repository.LectureRepository; @@ -30,10 +37,15 @@ class VideoUnitIntegrationTest extends AbstractSpringIntegrationIndependentTest @Autowired private LectureUtilService lectureUtilService; + @Autowired + private CompetencyUtilService competencyUtilService; + private Lecture lecture1; private VideoUnit videoUnit; + private Competency competency; + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 1); @@ -47,6 +59,8 @@ void initTestCase() { userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); userUtilService.createAndSaveUser(TEST_PREFIX + "tutor42"); userUtilService.createAndSaveUser(TEST_PREFIX + "instructor42"); + + competency = competencyUtilService.createCompetency(lecture1.getCourse()); } private void testAllPreAuthorize() throws Exception { @@ -71,8 +85,10 @@ void testAll_asStudent() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createVideoUnit_asInstructor_shouldCreateVideoUnit() throws Exception { videoUnit.setSource("https://www.youtube.com/embed/8iU8LPEa4o0"); + videoUnit.setCompetencies(Set.of(competency)); var persistedVideoUnit = request.postWithResponseBody("/api/lectures/" + this.lecture1.getId() + "/video-units", videoUnit, VideoUnit.class, HttpStatus.CREATED); assertThat(persistedVideoUnit.getId()).isNotNull(); + verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(persistedVideoUnit)); } @Test @@ -99,6 +115,7 @@ void createVideoUnit_InstructorNotInCourse_shouldReturnForbidden() throws Except @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateVideoUnit_asInstructor_shouldUpdateVideoUnit() throws Exception { + videoUnit.setCompetencies(Set.of(competency)); persistVideoUnitWithLecture(); this.videoUnit = (VideoUnit) lectureRepository.findByIdWithLectureUnitsAndAttachments(lecture1.getId()).orElseThrow().getLectureUnits().stream().findFirst().orElseThrow(); @@ -106,6 +123,7 @@ void updateVideoUnit_asInstructor_shouldUpdateVideoUnit() throws Exception { this.videoUnit.setDescription("Changed"); this.videoUnit = request.putWithResponseBody("/api/lectures/" + lecture1.getId() + "/video-units", this.videoUnit, VideoUnit.class, HttpStatus.OK); assertThat(this.videoUnit.getDescription()).isEqualTo("Changed"); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(videoUnit), eq(Optional.of(videoUnit))); } @Test @@ -178,12 +196,14 @@ void getVideoUnit_correctId_shouldReturnVideoUnit() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void deleteVideoUnit_correctId_shouldDeleteVideoUnit() throws Exception { + videoUnit.setCompetencies(Set.of(competency)); persistVideoUnitWithLecture(); this.videoUnit = (VideoUnit) lectureRepository.findByIdWithLectureUnitsAndAttachments(lecture1.getId()).orElseThrow().getLectureUnits().stream().findFirst().orElseThrow(); assertThat(this.videoUnit.getId()).isNotNull(); request.delete("/api/lectures/" + lecture1.getId() + "/lecture-units/" + this.videoUnit.getId(), HttpStatus.OK); request.get("/api/lectures/" + lecture1.getId() + "/video-units/" + this.videoUnit.getId(), HttpStatus.NOT_FOUND, VideoUnit.class); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(videoUnit), eq(Optional.empty())); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java index 49b01b70d18e..e661a5d888e5 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/TextExerciseIntegrationTest.java @@ -6,7 +6,9 @@ import static de.tum.in.www1.artemis.util.TestResourceUtils.HalfSecond; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -33,6 +35,7 @@ import org.springframework.security.test.context.support.WithMockUser; import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.competency.CompetencyUtilService; import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ExampleSubmission; import de.tum.in.www1.artemis.domain.Feedback; @@ -44,6 +47,7 @@ import de.tum.in.www1.artemis.domain.TextBlock; import de.tum.in.www1.artemis.domain.TextExercise; import de.tum.in.www1.artemis.domain.TextSubmission; +import de.tum.in.www1.artemis.domain.competency.Competency; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.DifficultyLevel; import de.tum.in.www1.artemis.domain.enumeration.ExerciseMode; @@ -129,18 +133,28 @@ class TextExerciseIntegrationTest extends AbstractSpringIntegrationIndependentTe @Autowired private PlagiarismUtilService plagiarismUtilService; + @Autowired + private CompetencyUtilService competencyUtilService; + + private Course course; + + private TextExercise textExercise; + + private Competency competency; + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 2, 1, 0, 1); userUtilService.addInstructor("other-instructors", TEST_PREFIX + "instructorother"); + course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); + textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); + competency = competencyUtilService.createCompetency(course); } @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void submitEnglishTextExercise() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("This Submission is written in English", Language.ENGLISH, false); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); request.postWithResponseBody("/api/exercises/" + textExercise.getId() + "/participations", null, Participation.class); textSubmission = request.postWithResponseBody("/api/exercises/" + textExercise.getId() + "/text-submissions", textSubmission, TextSubmission.class); @@ -152,8 +166,6 @@ void submitEnglishTextExercise() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void deleteTextExerciseWithSubmissionWithTextBlocks() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("Lorem Ipsum Foo Bar", Language.ENGLISH, true); textSubmission = textExerciseUtilService.saveTextSubmission(textExercise, textSubmission, TEST_PREFIX + "student1"); int submissionCount = 5; @@ -179,6 +191,17 @@ void testDeleteTextExerciseWithChannel() throws Exception { assertThat(exerciseChannelAfterDelete).isEmpty(); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteTextExerciseWithCompetency() throws Exception { + textExercise.setCompetencies(Set.of(competency)); + textExerciseRepository.save(textExercise); + + request.delete("/api/text-exercises/" + textExercise.getId(), HttpStatus.OK); + + verify(competencyProgressService).updateProgressByCompetencyAsync(eq(competency)); + } + @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void deleteExamTextExercise() throws Exception { @@ -200,8 +223,6 @@ void deleteTextExercise_notFound() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void deleteTextExercise_isNotAtLeastInstructorInCourse_forbidden() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); course.setInstructorGroupName("test"); courseRepository.save(course); @@ -213,9 +234,7 @@ void deleteTextExercise_isNotAtLeastInstructorInCourse_forbidden() throws Except @ValueSource(strings = { "exercise-new-text-exercise", "" }) @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createTextExercise(String channelName) throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); courseUtilService.enableMessagingForCourse(course); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); String title = "New Text Exercise"; DifficultyLevel difficulty = DifficultyLevel.HARD; @@ -248,8 +267,6 @@ void createTextExercise_setExerciseTitleNull_badRequest() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createTextExercise_setAssessmentDueDateWithoutExerciseDueDate_badRequest() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise.setId(null); textExercise.setDueDate(null); @@ -259,8 +276,6 @@ void createTextExercise_setAssessmentDueDateWithoutExerciseDueDate_badRequest() @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createTextExercise_isNotAtLeastInstructorInCourse_forbidden() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); course.setInstructorGroupName("test"); courseRepository.save(course); textExercise.setId(null); @@ -334,8 +349,6 @@ void createTextExercise_setNeitherCourseAndExerciseGroup_badRequest() throws Exc @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_InvalidMaxScore() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise.setMaxPoints(0.0); request.putWithResponseBody("/api/text-exercises", textExercise, TextExercise.class, HttpStatus.BAD_REQUEST); } @@ -343,8 +356,6 @@ void updateTextExercise_InvalidMaxScore() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_IncludedAsBonusInvalidBonusPoints() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise.setMaxPoints(10.0); textExercise.setBonusPoints(1.0); textExercise.setIncludedInOverallScore(IncludedInOverallScore.INCLUDED_AS_BONUS); @@ -354,8 +365,6 @@ void updateTextExercise_IncludedAsBonusInvalidBonusPoints() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_NotIncludedInvalidBonusPoints() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise.setMaxPoints(10.0); textExercise.setBonusPoints(1.0); textExercise.setIncludedInOverallScore(IncludedInOverallScore.NOT_INCLUDED); @@ -365,9 +374,6 @@ void updateTextExercise_NotIncludedInvalidBonusPoints() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_WithStructuredGradingInstructions() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - GradingCriterion criterion = new GradingCriterion(); criterion.setTitle("Test"); @@ -393,8 +399,6 @@ void updateTextExercise_WithStructuredGradingInstructions() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise = textExerciseRepository.findByIdWithExampleSubmissionsAndResultsElseThrow(textExercise.getId()); // update certain attributes of text exercise @@ -414,6 +418,7 @@ void updateTextExercise() throws Exception { exampleSubmission.setExercise(textExercise); exampleSubmissionRepo.save(exampleSubmission); textExercise.addExampleSubmission(exampleSubmission); + textExercise.setCompetencies(Set.of(competency)); TextExercise updatedTextExercise = request.putWithResponseBody("/api/text-exercises", textExercise, TextExercise.class, HttpStatus.OK); @@ -424,14 +429,12 @@ void updateTextExercise() throws Exception { assertThat(updatedTextExercise.getCourseViaExerciseGroupOrCourseMember().getId()).as("courseId was not updated").isEqualTo(course.getId()); verify(examLiveEventsService, never()).createAndSendProblemStatementUpdateEvent(any(), any(), any()); verify(groupNotificationScheduleService, times(1)).checkAndCreateAppropriateNotificationsWhenUpdatingExercise(any(), any(), any()); + verify(competencyProgressService, timeout(1000).times(1)).updateProgressForUpdatedLearningObjectAsync(eq(textExercise), eq(Optional.of(textExercise))); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExerciseDueDate() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - final TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - final ZonedDateTime individualDueDate = ZonedDateTime.now().plusHours(20); { @@ -464,8 +467,6 @@ void updateTextExerciseDueDate() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_setExerciseIdNull_created() throws Exception { - Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise.setId(null); textExercise.setChannelName("test" + UUID.randomUUID().toString().substring(0, 8)); request.putWithResponseBody("/api/text-exercises", textExercise, TextExercise.class, HttpStatus.CREATED); @@ -475,7 +476,6 @@ void updateTextExercise_setExerciseIdNull_created() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_updatingCourseId_asInstructor() throws Exception { // Create a text exercise. - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); TextExercise existingTextExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); // Create a new course with different id. @@ -494,8 +494,6 @@ void updateTextExercise_updatingCourseId_asInstructor() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_isNotAtLeastInstructorInCourse_forbidden() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); course.setInstructorGroupName("test"); courseRepository.save(course); @@ -543,9 +541,7 @@ void updateTextExerciseForExam_invalidExercise_dates(InvalidExamExerciseDateConf @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_setCourseAndExerciseGroup_badRequest() throws Exception { - Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); ExerciseGroup exerciseGroup = examUtilService.addExerciseGroupWithExamAndCourse(true); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise.setExerciseGroup(exerciseGroup); request.putWithResponseBody("/api/text-exercises", textExercise, TextExercise.class, HttpStatus.BAD_REQUEST); @@ -554,8 +550,6 @@ void updateTextExercise_setCourseAndExerciseGroup_badRequest() throws Exception @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_setNeitherCourseAndExerciseGroup_badRequest() throws Exception { - Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise.setCourse(null); request.putWithResponseBody("/api/text-exercises", textExercise, TextExercise.class, HttpStatus.BAD_REQUEST); @@ -564,8 +558,6 @@ void updateTextExercise_setNeitherCourseAndExerciseGroup_badRequest() throws Exc @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void updateTextExercise_convertFromCourseToExamExercise_badRequest() throws Exception { - Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); ExerciseGroup exerciseGroup = examUtilService.addExerciseGroupWithExamAndCourse(true); textExercise.setExerciseGroup(exerciseGroup); @@ -596,9 +588,11 @@ void importTextExerciseFromCourseToCourse() throws Exception { textExerciseRepository.save(textExercise); textExercise.setCourse(course2); textExercise.setChannelName("testchannel" + textExercise.getId()); + textExercise.setCompetencies(Set.of(competency)); var newTextExercise = request.postWithResponseBody("/api/text-exercises/import/" + textExercise.getId(), textExercise, TextExercise.class, HttpStatus.CREATED); Channel channel = channelRepository.findChannelByExerciseId(newTextExercise.getId()); assertThat(channel).isNotNull(); + verify(competencyProgressService).updateProgressByLearningObjectAsync(eq(newTextExercise)); } @Test @@ -752,8 +746,6 @@ void importTextExerciseFromCourseToCourse_exampleSolutionPublicationDate() throw @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void getAllTextExercisesForCourse() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - List textExercises = request.getList("/api/courses/" + course.getId() + "/text-exercises", HttpStatus.OK, TextExercise.class); assertThat(textExercises).as("text exercises for course were retrieved").hasSize(1); @@ -762,7 +754,6 @@ void getAllTextExercisesForCourse() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void getAllTextExercisesForCourse_isNotAtLeastTeachingAssistantInCourse_forbidden() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); course.setTeachingAssistantGroupName("test"); courseRepository.save(course); @@ -781,8 +772,6 @@ void getTextExercise_notFound() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void getTextExerciseAsTutor() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); Channel channel = new Channel(); channel.setIsPublic(true); channel.setIsAnnouncementChannel(false); @@ -820,8 +809,6 @@ void getExamTextExerciseAsInstructor() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void getTextExercise_isNotAtleastTeachingAssistantInCourse_forbidden() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); course.setTeachingAssistantGroupName("test"); courseRepository.save(course); request.get("/api/text-exercises/" + textExercise.getId(), HttpStatus.FORBIDDEN, TextExercise.class); @@ -830,9 +817,6 @@ void getTextExercise_isNotAtleastTeachingAssistantInCourse_forbidden() throws Ex @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetTextExercise_setGradingInstructionFeedbackUsed() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - Channel channel = new Channel(); channel.setName("testchannel-" + UUID.randomUUID().toString().substring(0, 8)); channel.setIsPublic(true); @@ -854,7 +838,6 @@ void testGetTextExercise_setGradingInstructionFeedbackUsed() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructorother1", roles = "INSTRUCTOR") void testInstructorGetsOnlyResultsFromOwningCourses() throws Exception { - textExerciseUtilService.addCourseWithOneReleasedTextExercise(); final var search = pageableSearchUtilService.configureSearch(""); final var result = request.getSearchResult("/api/text-exercises", HttpStatus.OK, TextExercise.class, pageableSearchUtilService.searchMapping(search)); assertThat(result.getResultsOnPage()).isNullOrEmpty(); @@ -1053,9 +1036,6 @@ void testImportTextExercise_individual_modeChange() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testCheckPlagiarismIdenticalLongTexts() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - var longText = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vitae vestibulum metus. @@ -1121,9 +1101,6 @@ void testCheckPlagiarismIdenticalLongTexts() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testCheckPlagiarismIdenticalShortTexts() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - var shortText = "Lorem Ipsum Foo Bar"; textExerciseUtilService.createSubmissionForTextExercise(textExercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1"), shortText); textExerciseUtilService.createSubmissionForTextExercise(textExercise, userUtilService.getUserByLogin(TEST_PREFIX + "student2"), shortText); @@ -1135,9 +1112,6 @@ void testCheckPlagiarismIdenticalShortTexts() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testCheckPlagiarismNoSubmissions() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - var path = "/api/text-exercises/" + textExercise.getId() + "/check-plagiarism"; request.get(path, HttpStatus.BAD_REQUEST, TextPlagiarismResult.class, plagiarismUtilService.getDefaultPlagiarismOptions()); } @@ -1145,8 +1119,6 @@ void testCheckPlagiarismNoSubmissions() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testCheckPlagiarism_isNotAtLeastInstructorInCourse_forbidden() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); course.setInstructorGroupName("test"); courseRepository.save(course); request.get("/api/text-exercises/" + textExercise.getId() + "/check-plagiarism", HttpStatus.FORBIDDEN, TextPlagiarismResult.class, @@ -1156,9 +1128,6 @@ void testCheckPlagiarism_isNotAtLeastInstructorInCourse_forbidden() throws Excep @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetPlagiarismResult() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - TextPlagiarismResult expectedResult = textExerciseUtilService.createTextPlagiarismResultForExercise(textExercise); var result = request.get("/api/text-exercises/" + textExercise.getId() + "/plagiarism-result", HttpStatus.OK, PlagiarismResultDTO.class); @@ -1168,8 +1137,6 @@ void testGetPlagiarismResult() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetPlagiarismResultWithoutResult() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); var result = request.get("/api/text-exercises/" + textExercise.getId() + "/plagiarism-result", HttpStatus.OK, String.class); assertThat(result).isNullOrEmpty(); } @@ -1184,8 +1151,6 @@ void testGetPlagiarismResultWithoutExercise() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetPlagiarismResult_isNotAtLeastInstructorInCourse_forbidden() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); course.setInstructorGroupName("test"); courseRepository.save(course); request.get("/api/text-exercises/" + textExercise.getId() + "/plagiarism-result", HttpStatus.FORBIDDEN, String.class); @@ -1194,8 +1159,6 @@ void testGetPlagiarismResult_isNotAtLeastInstructorInCourse_forbidden() throws E @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testReEvaluateAndUpdateTextExercise() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); Set gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(textExercise); gradingCriterionRepository.saveAll(gradingCriteria); @@ -1218,8 +1181,6 @@ void testReEvaluateAndUpdateTextExercise() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testReEvaluateAndUpdateTextExerciseWithExampleSubmission() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); Set gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(textExercise); gradingCriterionRepository.saveAll(gradingCriteria); gradingCriteria.remove(1); @@ -1244,8 +1205,6 @@ void testReEvaluateAndUpdateTextExerciseWithExampleSubmission() throws Exception @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testReEvaluateAndUpdateTextExercise_shouldDeleteFeedbacks() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); Set gradingCriteria = exerciseUtilService.addGradingInstructionsToExercise(textExercise); gradingCriterionRepository.saveAll(gradingCriteria); @@ -1266,8 +1225,6 @@ void testReEvaluateAndUpdateTextExercise_shouldDeleteFeedbacks() throws Exceptio @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testReEvaluateAndUpdateTextExercise_isNotAtLeastInstructorInCourse_forbidden() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); course.setInstructorGroupName("test"); courseRepository.save(course); @@ -1277,9 +1234,7 @@ void testReEvaluateAndUpdateTextExercise_isNotAtLeastInstructorInCourse_forbidde @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testReEvaluateAndUpdateTextExercise_isNotSameGivenExerciseIdInRequestBody_conflict() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - TextExercise textExerciseToBeConflicted = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); + TextExercise textExerciseToBeConflicted = textExerciseRepository.findByCourseIdWithCategories(course.getId()).get(0); textExerciseToBeConflicted.setId(123456789L); textExerciseRepository.save(textExerciseToBeConflicted); @@ -1289,9 +1244,6 @@ void testReEvaluateAndUpdateTextExercise_isNotSameGivenExerciseIdInRequestBody_c @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testReEvaluateAndUpdateTextExercise_notFound() throws Exception { - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - request.putWithResponseBody("/api/text-exercises/" + 123456789 + "/re-evaluate", textExercise, TextExercise.class, HttpStatus.NOT_FOUND); } @@ -1299,8 +1251,6 @@ void testReEvaluateAndUpdateTextExercise_notFound() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createTextExercise_setInvalidExampleSolutionPublicationDate_badRequest() throws Exception { final var baseTime = ZonedDateTime.now(); - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise.setId(null); textExercise.setAssessmentDueDate(null); textExercise.setIncludedInOverallScore(IncludedInOverallScore.INCLUDED_COMPLETELY); @@ -1322,8 +1272,6 @@ void createTextExercise_setInvalidExampleSolutionPublicationDate_badRequest() th @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createTextExercise_setValidExampleSolutionPublicationDate() throws Exception { final var baseTime = ZonedDateTime.now(); - final Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); textExercise.setId(null); textExercise.setAssessmentDueDate(null); textExercise.setIncludedInOverallScore(IncludedInOverallScore.INCLUDED_COMPLETELY); @@ -1410,9 +1358,6 @@ void testImportTextExercise_setGradingInstructionForCopiedFeedback() throws Exce } private void testGetTextExercise_exampleSolutionVisibility(boolean isStudent, String username) throws Exception { - Course course = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); - final TextExercise textExercise = textExerciseRepository.findByCourseIdWithCategories(course.getId()).getFirst(); - // Utility function to avoid duplication Function textExerciseGetter = c -> (TextExercise) c.getExercises().stream().filter(e -> e.getId().equals(textExercise.getId())).findAny() .orElseThrow();