From 0113946c570db4a8cb7fb570449725458db91864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20G=C3=B6=C3=9Fmann?= Date: Thu, 16 Jan 2025 16:50:30 +0100 Subject: [PATCH] Adaptive learning: Add learner profile (#9673) --- .../artemis/atlas/api/LearnerProfileApi.java | 51 ++++++++++++ .../domain/profile/CourseLearnerProfile.java | 82 +++++++++++++++++++ .../atlas/domain/profile/LearnerProfile.java | 54 ++++++++++++ .../CourseLearnerProfileRepository.java | 32 ++++++++ .../repository/LearnerProfileRepository.java | 29 +++++++ .../learningpath/LearningPathService.java | 23 +++++- .../profile/CourseLearnerProfileService.java | 82 +++++++++++++++++++ .../profile/LearnerProfileService.java | 46 +++++++++++ .../tum/cit/aet/artemis/core/domain/User.java | 15 ++++ .../core/repository/UserRepository.java | 13 +++ .../artemis/core/service/CourseService.java | 27 ++++-- .../core/service/UserScheduleService.java | 8 +- .../service/user/UserCreationService.java | 11 ++- .../core/service/user/UserService.java | 8 +- .../aet/artemis/core/web/CourseResource.java | 26 +++--- .../changelog/20241107130000_changelog.xml | 74 +++++++++++++++++ .../resources/config/liquibase/master.xml | 1 + .../atlas/AbstractAtlasIntegrationTest.java | 4 + .../LearningPathIntegrationTest.java | 7 +- .../LearnerProfileIntegrationTest.java | 38 +++++++++ .../util/LearnerProfileUtilService.java | 27 ++++++ .../test_repository/UserTestRepository.java | 11 ++- .../core/user/util/UserTestService.java | 30 ++++--- ...ogrammingIntegrationJenkinsGitlabTest.java | 4 + .../CourseGitlabJenkinsIntegrationTest.java | 1 + 25 files changed, 663 insertions(+), 41 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/atlas/api/LearnerProfileApi.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/CourseLearnerProfile.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/LearnerProfile.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseLearnerProfileRepository.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearnerProfileRepository.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/atlas/service/profile/CourseLearnerProfileService.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/atlas/service/profile/LearnerProfileService.java create mode 100644 src/main/resources/config/liquibase/changelog/20241107130000_changelog.xml create mode 100644 src/test/java/de/tum/cit/aet/artemis/atlas/profile/LearnerProfileIntegrationTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/atlas/profile/util/LearnerProfileUtilService.java diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/api/LearnerProfileApi.java b/src/main/java/de/tum/cit/aet/artemis/atlas/api/LearnerProfileApi.java new file mode 100644 index 000000000000..d3099a4ba970 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/api/LearnerProfileApi.java @@ -0,0 +1,51 @@ +package de.tum.cit.aet.artemis.atlas.api; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.util.Set; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; + +import de.tum.cit.aet.artemis.atlas.service.profile.CourseLearnerProfileService; +import de.tum.cit.aet.artemis.atlas.service.profile.LearnerProfileService; +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.domain.User; + +@Controller +@Profile(PROFILE_CORE) +public class LearnerProfileApi extends AbstractAtlasApi { + + private final LearnerProfileService learnerProfileService; + + private final CourseLearnerProfileService courseLearnerProfileService; + + public LearnerProfileApi(LearnerProfileService learnerProfileService, CourseLearnerProfileService courseLearnerProfileService) { + this.learnerProfileService = learnerProfileService; + this.courseLearnerProfileService = courseLearnerProfileService; + } + + public void deleteAllForCourse(Course course) { + courseLearnerProfileService.deleteAllForCourse(course); + } + + public void createCourseLearnerProfile(Course course, User user) { + courseLearnerProfileService.createCourseLearnerProfile(course, user); + } + + public void createCourseLearnerProfiles(Course course, Set students) { + courseLearnerProfileService.createCourseLearnerProfiles(course, students); + } + + public void deleteCourseLearnerProfile(Course course, User user) { + courseLearnerProfileService.deleteCourseLearnerProfile(course, user); + } + + public void createProfile(User user) { + learnerProfileService.createProfile(user); + } + + public void deleteProfile(User user) { + learnerProfileService.deleteProfile(user); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/CourseLearnerProfile.java b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/CourseLearnerProfile.java new file mode 100644 index 000000000000..91e77a7c44e1 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/CourseLearnerProfile.java @@ -0,0 +1,82 @@ +package de.tum.cit.aet.artemis.atlas.domain.profile; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; + +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.domain.DomainObject; + +@Entity +@Table(name = "course_learner_profile") +public class CourseLearnerProfile extends DomainObject { + + @ManyToOne + @JoinColumn(name = "learner_profile_id") + private LearnerProfile learnerProfile; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "course_id") + private Course course; + + @Column(name = "aim_for_grade_or_bonus") + @Min(0) + @Max(5) + private int aimForGradeOrBonus; + + @Column(name = "time_investment") + @Min(0) + @Max(5) + private int timeInvestment; + + @Column(name = "repetition_intensity") + @Min(0) + @Max(5) + private int repetitionIntensity; + + public void setLearnerProfile(LearnerProfile learnerProfile) { + this.learnerProfile = learnerProfile; + } + + public LearnerProfile getLearnerProfile() { + return this.learnerProfile; + } + + public void setCourse(Course course) { + this.course = course; + } + + public Course getCourse() { + return this.course; + } + + public int getAimForGradeOrBonus() { + return aimForGradeOrBonus; + } + + public void setAimForGradeOrBonus(int aimForGradeOrBonus) { + this.aimForGradeOrBonus = aimForGradeOrBonus; + } + + public int getTimeInvestment() { + return timeInvestment; + } + + public void setTimeInvestment(int timeInvestment) { + this.timeInvestment = timeInvestment; + } + + public int getRepetitionIntensity() { + return repetitionIntensity; + } + + public void setRepetitionIntensity(int repetitionIntensity) { + this.repetitionIntensity = repetitionIntensity; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/LearnerProfile.java b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/LearnerProfile.java new file mode 100644 index 000000000000..84bed662672d --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/LearnerProfile.java @@ -0,0 +1,54 @@ +package de.tum.cit.aet.artemis.atlas.domain.profile; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import de.tum.cit.aet.artemis.core.domain.DomainObject; +import de.tum.cit.aet.artemis.core.domain.User; + +@Entity +@Table(name = "learner_profile") +public class LearnerProfile extends DomainObject { + + @OneToOne(mappedBy = "learnerProfile", cascade = CascadeType.PERSIST) + private User user; + + @OneToMany(mappedBy = "learnerProfile", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + private Set courseLearnerProfiles = new HashSet<>(); + + public void setUser(User user) { + this.user = user; + } + + public User getUser() { + return this.user; + } + + public void setCourseLearnerProfiles(Set courseLearnerProfiles) { + this.courseLearnerProfiles = courseLearnerProfiles; + } + + public Set getCourseLearnerProfiles() { + return this.courseLearnerProfiles; + } + + public boolean addCourseLearnerProfile(CourseLearnerProfile courseLearnerProfile) { + return this.courseLearnerProfiles.add(courseLearnerProfile); + } + + public boolean addAllCourseLearnerProfiles(Collection courseLearnerProfiles) { + return this.courseLearnerProfiles.addAll(courseLearnerProfiles); + } + + public boolean removeCourseLearnerProfile(CourseLearnerProfile courseLearnerProfile) { + return this.courseLearnerProfiles.remove(courseLearnerProfile); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseLearnerProfileRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseLearnerProfileRepository.java new file mode 100644 index 000000000000..c58cb82f6b96 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseLearnerProfileRepository.java @@ -0,0 +1,32 @@ +package de.tum.cit.aet.artemis.atlas.repository; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import de.tum.cit.aet.artemis.atlas.domain.profile.CourseLearnerProfile; +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; + +@Profile(PROFILE_CORE) +@Repository +public interface CourseLearnerProfileRepository extends ArtemisJpaRepository { + + @Transactional // ok because of delete + @Modifying + @Query(""" + DELETE FROM CourseLearnerProfile clp + WHERE clp.course = :course AND clp.learnerProfile.user = :user + """) + void deleteByCourseAndUser(@Param("course") Course course, @Param("user") User user); + + @Transactional // ok because of delete + @Modifying + void deleteAllByCourse(Course couese); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearnerProfileRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearnerProfileRepository.java new file mode 100644 index 000000000000..e08712461584 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearnerProfileRepository.java @@ -0,0 +1,29 @@ +package de.tum.cit.aet.artemis.atlas.repository; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.util.Optional; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import de.tum.cit.aet.artemis.atlas.domain.profile.LearnerProfile; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; + +@Profile(PROFILE_CORE) +@Repository +public interface LearnerProfileRepository extends ArtemisJpaRepository { + + Optional findByUser(User user); + + default LearnerProfile findByUserElseThrow(User user) { + return getValueElseThrow(findByUser(user)); + } + + @Transactional // ok because of delete + @Modifying + void deleteByUser(User user); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java index 083c06123a45..8048d2c04ab8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathService.java @@ -37,6 +37,7 @@ import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; import de.tum.cit.aet.artemis.atlas.repository.LearningPathRepository; import de.tum.cit.aet.artemis.atlas.service.competency.CompetencyProgressService; +import de.tum.cit.aet.artemis.atlas.service.profile.CourseLearnerProfileService; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; @@ -90,10 +91,13 @@ public class LearningPathService { private final CourseCompetencyRepository courseCompetencyRepository; + private final CourseLearnerProfileService courseLearnerProfileService; + public LearningPathService(UserRepository userRepository, LearningPathRepository learningPathRepository, CompetencyProgressRepository competencyProgressRepository, LearningPathNavigationService learningPathNavigationService, CourseRepository courseRepository, CompetencyRepository competencyRepository, CompetencyRelationRepository competencyRelationRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, - StudentParticipationRepository studentParticipationRepository, CourseCompetencyRepository courseCompetencyRepository) { + StudentParticipationRepository studentParticipationRepository, CourseCompetencyRepository courseCompetencyRepository, + CourseLearnerProfileService courseLearnerProfileService) { this.userRepository = userRepository; this.learningPathRepository = learningPathRepository; this.competencyProgressRepository = competencyProgressRepository; @@ -104,6 +108,7 @@ public LearningPathService(UserRepository userRepository, LearningPathRepository this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; this.studentParticipationRepository = studentParticipationRepository; this.courseCompetencyRepository = courseCompetencyRepository; + this.courseLearnerProfileService = courseLearnerProfileService; } /** @@ -113,7 +118,9 @@ public LearningPathService(UserRepository userRepository, LearningPathRepository */ public void enableLearningPathsForCourse(@NotNull Course course) { course.setLearningPathsEnabled(true); - generateLearningPaths(course); + Set students = userRepository.getStudentsWithLearnerProfile(course); + courseLearnerProfileService.createCourseLearnerProfiles(course, students); + generateLearningPaths(course, students); courseRepository.save(course); log.debug("Enabled learning paths for course (id={})", course.getId()); } @@ -124,7 +131,17 @@ public void enableLearningPathsForCourse(@NotNull Course course) { * @param course course the learning paths are created for */ public void generateLearningPaths(@NotNull Course course) { - var students = userRepository.getStudents(course); + Set students = userRepository.getStudentsWithLearnerProfile(course); + generateLearningPaths(course, students); + } + + /** + * Generate learning paths for all students enrolled in the course + * + * @param course course the learning paths are created for + * @param students students for which the learning paths are generated with eager loaded learner profiles + */ + public void generateLearningPaths(@NotNull Course course, Set students) { students.forEach(student -> generateLearningPathForUser(course, student)); log.debug("Successfully created learning paths for all {} students in course (id={})", students.size(), course.getId()); } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/profile/CourseLearnerProfileService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/profile/CourseLearnerProfileService.java new file mode 100644 index 000000000000..8ca20bec626e --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/profile/CourseLearnerProfileService.java @@ -0,0 +1,82 @@ +package de.tum.cit.aet.artemis.atlas.service.profile; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.atlas.domain.profile.CourseLearnerProfile; +import de.tum.cit.aet.artemis.atlas.repository.CourseLearnerProfileRepository; +import de.tum.cit.aet.artemis.atlas.repository.LearnerProfileRepository; +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.domain.User; + +@Profile(PROFILE_CORE) +@Service +public class CourseLearnerProfileService { + + private final CourseLearnerProfileRepository courseLearnerProfileRepository; + + private final LearnerProfileRepository learnerProfileRepository; + + public CourseLearnerProfileService(CourseLearnerProfileRepository courseLearnerProfileRepository, LearnerProfileRepository learnerProfileRepository) { + this.courseLearnerProfileRepository = courseLearnerProfileRepository; + this.learnerProfileRepository = learnerProfileRepository; + } + + /** + * Create a course learner profile for a user and saves it in the database + * + * @param course the course for which the profile is created + * @param user the user for which the profile is created + */ + public void createCourseLearnerProfile(Course course, User user) { + var courseProfile = new CourseLearnerProfile(); + courseProfile.setCourse(course); + + var learnerProfile = learnerProfileRepository.findByUserElseThrow(user); + courseProfile.setLearnerProfile(learnerProfile); + + courseLearnerProfileRepository.save(courseProfile); + } + + /** + * Create course learner profiles for a set of users and saves them in the database. + * + * @param course the course for which the profiles are created + * @param users the users for which the profiles are created with eagerly loaded learner profiles + */ + public void createCourseLearnerProfiles(Course course, Set users) { + Set courseProfiles = users.stream().map(user -> { + var courseProfile = new CourseLearnerProfile(); + courseProfile.setCourse(course); + courseProfile.setLearnerProfile(user.getLearnerProfile()); + + return courseProfile; + }).collect(Collectors.toSet()); + + courseLearnerProfileRepository.saveAll(courseProfiles); + } + + /** + * Delete a course learner profile for a user + * + * @param course the course for which the profile is deleted + * @param user the user for which the profile is deleted + */ + public void deleteCourseLearnerProfile(Course course, User user) { + courseLearnerProfileRepository.deleteByCourseAndUser(course, user); + } + + /** + * Delete all course learner profiles for a course + * + * @param course the course for which the profiles are deleted + */ + public void deleteAllForCourse(Course course) { + courseLearnerProfileRepository.deleteAllByCourse(course); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/profile/LearnerProfileService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/profile/LearnerProfileService.java new file mode 100644 index 000000000000..5b92074b1c0d --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/profile/LearnerProfileService.java @@ -0,0 +1,46 @@ +package de.tum.cit.aet.artemis.atlas.service.profile; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.atlas.domain.profile.LearnerProfile; +import de.tum.cit.aet.artemis.atlas.repository.LearnerProfileRepository; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.repository.UserRepository; + +@Profile(PROFILE_CORE) +@Service +public class LearnerProfileService { + + private final LearnerProfileRepository learnerProfileRepository; + + private final UserRepository userRepository; + + public LearnerProfileService(LearnerProfileRepository learnerProfileRepository, UserRepository userRepository) { + this.learnerProfileRepository = learnerProfileRepository; + this.userRepository = userRepository; + } + + /** + * Create a learner profile for a user and saves it in the database + * + * @param user the user for which the profile is created + */ + public void createProfile(User user) { + var profile = new LearnerProfile(); + profile.setUser(user); + user.setLearnerProfile(profile); + userRepository.save(user); + } + + /** + * Delete the learner profile of a user + * + * @param user the user for which the profile is deleted + */ + public void deleteProfile(User user) { + learnerProfileRepository.deleteByUser(user); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java index cebc42aafde9..e6c05978126b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java @@ -21,6 +21,7 @@ import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.validation.constraints.Email; @@ -40,6 +41,7 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyProgress; import de.tum.cit.aet.artemis.atlas.domain.competency.LearningPath; +import de.tum.cit.aet.artemis.atlas.domain.profile.LearnerProfile; import de.tum.cit.aet.artemis.communication.domain.SavedPost; import de.tum.cit.aet.artemis.communication.domain.push_notification.PushNotificationDeviceConfiguration; import de.tum.cit.aet.artemis.core.config.Constants; @@ -211,6 +213,11 @@ public class User extends AbstractAuditingEntity implements Participant { @Column(name = "iris_accepted") private ZonedDateTime irisAccepted = null; + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnoreProperties("user") + @JoinColumn(name = "learner_profile_id") + private LearnerProfile learnerProfile; + public User() { } @@ -547,4 +554,12 @@ public void hasAcceptedIrisElseThrow() { throw new AccessForbiddenException("The user has not accepted the Iris privacy policy yet."); } } + + public LearnerProfile getLearnerProfile() { + return learnerProfile; + } + + public void setLearnerProfile(LearnerProfile learnerProfile) { + this.learnerProfile = learnerProfile; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java index 6eb94330acff..ddf54cd99117 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java @@ -178,6 +178,9 @@ OR LOWER(user.login) = LOWER(:searchInput) @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Set findAllWithGroupsAndAuthoritiesByIsDeletedIsFalseAndGroupsContains(String groupName); + @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities", "learnerProfile" }) + Set findAllWithGroupsAndAuthoritiesAndLearnerProfileByIsDeletedIsFalseAndGroupsContains(String groupName); + @Query(""" SELECT DISTINCT user FROM User user @@ -995,6 +998,16 @@ default Set getStudents(Course course) { return findAllWithGroupsAndAuthoritiesByIsDeletedIsFalseAndGroupsContains(course.getStudentGroupName()); } + /** + * Get students by given course with their learner Profile + * + * @param course object + * @return students for given course + */ + default Set getStudentsWithLearnerProfile(Course course) { + return findAllWithGroupsAndAuthoritiesAndLearnerProfileByIsDeletedIsFalseAndGroupsContains(course.getStudentGroupName()); + } + /** * Get tutors by given course * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index 6122b92810c2..b60d01d40313 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -55,6 +55,7 @@ import de.tum.cit.aet.artemis.assessment.service.TutorLeaderboardService; import de.tum.cit.aet.artemis.atlas.api.CompetencyProgressApi; import de.tum.cit.aet.artemis.atlas.api.CompetencyRelationApi; +import de.tum.cit.aet.artemis.atlas.api.LearnerProfileApi; import de.tum.cit.aet.artemis.atlas.api.LearningPathApi; import de.tum.cit.aet.artemis.atlas.api.PrerequisitesApi; import de.tum.cit.aet.artemis.communication.domain.FaqState; @@ -127,8 +128,6 @@ public class CourseService { private static final Logger log = LoggerFactory.getLogger(CourseService.class); - private final FaqRepository faqRepository; - @Value("${artemis.course-archives-path}") private Path courseArchivesDirPath; @@ -216,6 +215,10 @@ public class CourseService { private final BuildJobRepository buildJobRepository; + private final FaqRepository faqRepository; + + private final LearnerProfileApi learnerProfileApi; + private final LLMTokenUsageTraceRepository llmTokenUsageTraceRepository; public CourseService(CourseRepository courseRepository, ExerciseService exerciseService, ExerciseDeletionService exerciseDeletionService, @@ -231,7 +234,8 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise LearningPathApi learningPathApi, Optional irisSettingsService, LectureRepository lectureRepository, TutorialGroupNotificationRepository tutorialGroupNotificationRepository, TutorialGroupChannelManagementService tutorialGroupChannelManagementService, PrerequisitesApi prerequisitesApi, CompetencyRelationApi competencyRelationApi, PostRepository postRepository, AnswerPostRepository answerPostRepository, - BuildJobRepository buildJobRepository, FaqRepository faqRepository, LLMTokenUsageTraceRepository llmTokenUsageTraceRepository) { + BuildJobRepository buildJobRepository, FaqRepository faqRepository, LearnerProfileApi learnerProfileApi, LLMTokenUsageTraceRepository llmTokenUsageTraceRepository) { + this.courseRepository = courseRepository; this.exerciseService = exerciseService; this.exerciseDeletionService = exerciseDeletionService; @@ -275,6 +279,7 @@ public CourseService(CourseRepository courseRepository, ExerciseService exercise this.postRepository = postRepository; this.answerPostRepository = answerPostRepository; this.faqRepository = faqRepository; + this.learnerProfileApi = learnerProfileApi; this.llmTokenUsageTraceRepository = llmTokenUsageTraceRepository; } @@ -526,6 +531,7 @@ public void delete(Course course) { deleteExamsOfCourse(course); deleteGradingScaleOfCourse(course); deleteFaqsOfCourse(course); + learnerProfileApi.deleteAllForCourse(course); irisSettingsService.ifPresent(iss -> iss.deleteSettingsFor(course)); courseRepository.deleteById(course.getId()); log.debug("Successfully deleted course {}.", course.getTitle()); @@ -636,6 +642,7 @@ public void enrollUserForCourseOrThrow(User user, Course course) { authCheckService.checkUserAllowedToEnrollInCourseElseThrow(user, course); userService.addUserToGroup(user, course.getStudentGroupName()); if (course.getLearningPathsEnabled()) { + learnerProfileApi.createCourseLearnerProfile(course, user); learningPathApi.generateLearningPathForUser(course, user); } final var auditEvent = new AuditEvent(user.getLogin(), Constants.ENROLL_IN_COURSE, "course=" + course.getTitle()); @@ -669,6 +676,7 @@ public List registerUsersForCourseGroup(Long courseId, List searchOtherUsersNameInCourse(Course course, String nameOfUser) /** * adds a given user to a user group * - * @param user user to be added to a group - * @param group user-group where the user should be added + * @param user user to be added to a group + * @param group user-group where the user should be added + * @param course the course in which the user should be added */ - public void addUserToGroup(User user, String group) { + public void addUserToGroup(User user, String group, Course course) { userService.addUserToGroup(user, group); + if (group.equals(course.getStudentGroupName()) && course.getLearningPathsEnabled()) { + Course courseWithCompetencies = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(course.getId()); + learnerProfileApi.createCourseLearnerProfile(course, user); + learningPathApi.generateLearningPathForUser(courseWithCompetencies, user); + } } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/UserScheduleService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/UserScheduleService.java index ffbe1ec288fa..d9004f8febb6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/UserScheduleService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/UserScheduleService.java @@ -17,6 +17,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.atlas.api.LearnerProfileApi; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.VersionControlException; import de.tum.cit.aet.artemis.core.repository.UserRepository; @@ -39,15 +40,19 @@ public class UserScheduleService { private final ScheduledExecutorService scheduler; + private final LearnerProfileApi learnerProfileApi; + // Used for tracking and canceling the non-activated accounts that will be cleaned up. // The key of the map is the user id. private final Map> nonActivatedAccountsFutures = new ConcurrentHashMap<>(); - public UserScheduleService(UserRepository userRepository, Optional optionalVcsUserManagementService, CacheManager cacheManager) { + public UserScheduleService(UserRepository userRepository, Optional optionalVcsUserManagementService, CacheManager cacheManager, + LearnerProfileApi learnerProfileApi) { this.userRepository = userRepository; this.optionalVcsUserManagementService = optionalVcsUserManagementService; this.cacheManager = cacheManager; this.scheduler = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); + this.learnerProfileApi = learnerProfileApi; } /** @@ -114,6 +119,7 @@ private void deleteUser(User user) { userRepository.delete(user); clearUserCaches(user); userRepository.flush(); + learnerProfileApi.deleteProfile(user); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserCreationService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserCreationService.java index 12133bfb8800..3355406a39bb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserCreationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserCreationService.java @@ -23,6 +23,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.atlas.api.LearnerProfileApi; import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.Authority; import de.tum.cit.aet.artemis.core.domain.Organization; @@ -74,9 +75,11 @@ public class UserCreationService { private final CacheManager cacheManager; + private final LearnerProfileApi learnerProfileApi; + public UserCreationService(UserRepository userRepository, PasswordService passwordService, AuthorityRepository authorityRepository, CourseRepository courseRepository, Optional optionalVcsUserManagementService, Optional optionalCIUserManagementService, CacheManager cacheManager, - OrganizationRepository organizationRepository) { + OrganizationRepository organizationRepository, LearnerProfileApi learnerProfileApi) { this.userRepository = userRepository; this.passwordService = passwordService; this.authorityRepository = authorityRepository; @@ -85,6 +88,7 @@ public UserCreationService(UserRepository userRepository, PasswordService passwo this.optionalCIUserManagementService = optionalCIUserManagementService; this.cacheManager = cacheManager; this.organizationRepository = organizationRepository; + this.learnerProfileApi = learnerProfileApi; } /** @@ -142,7 +146,8 @@ public User createUser(String login, @Nullable String password, @Nullable Set ldapUserService, GuidedTourSettingsRepository guidedTourSettingsRepository, PasswordService passwordService, Optional optionalVcsUserManagementService, Optional optionalCIUserManagementService, InstanceMessageSendService instanceMessageSendService, FileService fileService, ScienceEventApi scienceEventApi, - ParticipationVcsAccessTokenService participationVCSAccessTokenService, SavedPostRepository savedPostRepository) { + ParticipationVcsAccessTokenService participationVCSAccessTokenService, LearnerProfileApi learnerProfileApi, SavedPostRepository savedPostRepository) { this.userCreationService = userCreationService; this.userRepository = userRepository; this.authorityService = authorityService; @@ -136,6 +139,7 @@ public UserService(UserCreationService userCreationService, UserRepository userR this.fileService = fileService; this.scienceEventApi = scienceEventApi; this.participationVCSAccessTokenService = participationVCSAccessTokenService; + this.learnerProfileApi = learnerProfileApi; this.savedPostRepository = savedPostRepository; } @@ -469,7 +473,9 @@ public void updateUserInConnectorsAndAuthProvider(User user, String oldUserLogin public void softDeleteUser(String login) { userRepository.findOneWithGroupsByLogin(login).ifPresent(user -> { participationVCSAccessTokenService.deleteAllByUserId(user.getId()); + learnerProfileApi.deleteProfile(user); user.setDeleted(true); + user.setLearnerProfile(null); anonymizeUser(user); log.warn("Soft Deleted User: {}", user); }); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index 3523bcb48608..9880ba867b40 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -66,6 +66,7 @@ import de.tum.cit.aet.artemis.assessment.service.CourseScoreCalculationService; import de.tum.cit.aet.artemis.assessment.service.GradingScaleService; import de.tum.cit.aet.artemis.athena.service.AthenaModuleService; +import de.tum.cit.aet.artemis.atlas.api.LearnerProfileApi; import de.tum.cit.aet.artemis.atlas.api.LearningPathApi; import de.tum.cit.aet.artemis.communication.service.ConductAgreementService; import de.tum.cit.aet.artemis.core.config.Constants; @@ -177,6 +178,8 @@ public class CourseResource { private final Optional athenaModuleService; + private final LearnerProfileApi learnerProfileApi; + @Value("${artemis.course-archives-path}") private String courseArchivesDirPath; @@ -195,7 +198,7 @@ public CourseResource(UserRepository userRepository, CourseService courseService FileService fileService, TutorialGroupsConfigurationService tutorialGroupsConfigurationService, GradingScaleService gradingScaleService, CourseScoreCalculationService courseScoreCalculationService, GradingScaleRepository gradingScaleRepository, LearningPathApi learningPathApi, ConductAgreementService conductAgreementService, Optional athenaModuleService, ExamRepository examRepository, ComplaintService complaintService, - TeamRepository teamRepository) { + TeamRepository teamRepository, LearnerProfileApi learnerProfileApi) { this.courseService = courseService; this.courseRepository = courseRepository; this.exerciseService = exerciseService; @@ -219,6 +222,7 @@ public CourseResource(UserRepository userRepository, CourseService courseService this.examRepository = examRepository; this.complaintService = complaintService; this.teamRepository = teamRepository; + this.learnerProfileApi = learnerProfileApi; } /** @@ -335,6 +339,8 @@ else if (courseUpdate.getCourseIcon() == null && existingCourse.getCourseIcon() // if learning paths got enabled, generate learning paths for students if (existingCourse.getLearningPathsEnabled() != courseUpdate.getLearningPathsEnabled() && courseUpdate.getLearningPathsEnabled()) { Course courseWithCompetencies = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(result.getId()); + Set students = userRepository.getStudentsWithLearnerProfile(courseWithCompetencies); + learnerProfileApi.createCourseLearnerProfiles(courseWithCompetencies, students); learningPathApi.generateLearningPaths(courseWithCompetencies); } @@ -1175,7 +1181,7 @@ public ResponseEntity getCourseTitle(@PathVariable Long courseId) { public ResponseEntity addStudentToCourse(@PathVariable Long courseId, @PathVariable String studentLogin) { log.debug("REST request to add {} as student to course : {}", studentLogin, courseId); var course = courseRepository.findByIdElseThrow(courseId); - return addUserToCourseGroup(studentLogin, userRepository.getUserWithGroupsAndAuthorities(), course, course.getStudentGroupName(), Role.STUDENT); + return addUserToCourseGroup(studentLogin, userRepository.getUserWithGroupsAndAuthorities(), course, course.getStudentGroupName()); } /** @@ -1190,7 +1196,7 @@ public ResponseEntity addStudentToCourse(@PathVariable Long courseId, @Pat public ResponseEntity addTutorToCourse(@PathVariable Long courseId, @PathVariable String tutorLogin) { log.debug("REST request to add {} as tutors to course : {}", tutorLogin, courseId); var course = courseRepository.findByIdElseThrow(courseId); - return addUserToCourseGroup(tutorLogin, userRepository.getUserWithGroupsAndAuthorities(), course, course.getTeachingAssistantGroupName(), Role.TEACHING_ASSISTANT); + return addUserToCourseGroup(tutorLogin, userRepository.getUserWithGroupsAndAuthorities(), course, course.getTeachingAssistantGroupName()); } /** @@ -1206,7 +1212,7 @@ public ResponseEntity addEditorToCourse(@PathVariable Long courseId, @Path log.debug("REST request to add {} as editors to course : {}", editorLogin, courseId); Course course = courseRepository.findByIdElseThrow(courseId); courseService.checkIfEditorGroupsNeedsToBeCreated(course); - return addUserToCourseGroup(editorLogin, userRepository.getUserWithGroupsAndAuthorities(), course, course.getEditorGroupName(), Role.EDITOR); + return addUserToCourseGroup(editorLogin, userRepository.getUserWithGroupsAndAuthorities(), course, course.getEditorGroupName()); } /** @@ -1221,7 +1227,7 @@ public ResponseEntity addEditorToCourse(@PathVariable Long courseId, @Path public ResponseEntity addInstructorToCourse(@PathVariable Long courseId, @PathVariable String instructorLogin) { log.debug("REST request to add {} as instructors to course : {}", instructorLogin, courseId); var course = courseRepository.findByIdElseThrow(courseId); - return addUserToCourseGroup(instructorLogin, userRepository.getUserWithGroupsAndAuthorities(), course, course.getInstructorGroupName(), Role.INSTRUCTOR); + return addUserToCourseGroup(instructorLogin, userRepository.getUserWithGroupsAndAuthorities(), course, course.getInstructorGroupName()); } /** @@ -1231,21 +1237,17 @@ public ResponseEntity addInstructorToCourse(@PathVariable Long courseId, @ * @param instructorOrAdmin the user who initiates this request who must be an instructor of the given course or an admin * @param course the course which is only passes to check if the instructorOrAdmin is an instructor of the course * @param group the group to which the userLogin should be added - * @param role the role which should be added * @return empty ResponseEntity with status 200 (OK) or with status 404 (Not Found) or with status 403 (Forbidden) */ @NotNull - public ResponseEntity addUserToCourseGroup(String userLogin, User instructorOrAdmin, Course course, String group, Role role) { + public ResponseEntity addUserToCourseGroup(String userLogin, User instructorOrAdmin, Course course, String group) { if (authCheckService.isAtLeastInstructorInCourse(course, instructorOrAdmin)) { Optional userToAddToGroup = userRepository.findOneWithGroupsAndAuthoritiesByLogin(userLogin); if (userToAddToGroup.isEmpty()) { throw new EntityNotFoundException("User", userLogin); } - courseService.addUserToGroup(userToAddToGroup.get(), group); - if (role == Role.STUDENT && course.getLearningPathsEnabled()) { - Course courseWithCompetencies = courseRepository.findWithEagerCompetenciesAndPrerequisitesByIdElseThrow(course.getId()); - learningPathApi.generateLearningPathForUser(courseWithCompetencies, userToAddToGroup.get()); - } + User user = userToAddToGroup.get(); + courseService.addUserToGroup(user, group, course); return ResponseEntity.ok().body(null); } else { diff --git a/src/main/resources/config/liquibase/changelog/20241107130000_changelog.xml b/src/main/resources/config/liquibase/changelog/20241107130000_changelog.xml new file mode 100644 index 000000000000..0341d36332ca --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241107130000_changelog.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + INSERT INTO learner_profile (id) + SELECT u.id + FROM jhi_user u; + + + + UPDATE jhi_user u + SET learner_profile_id = u.id; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO course_learner_profile (learner_profile_id, course_id) + SELECT lp.id, c.id + FROM learner_profile lp, course c + WHERE c.start_date <= NOW() + AND c.end_date >= NOW() + AND c.test_course = FALSE + AND c.learning_paths_enabled = TRUE; + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index c25b722955e3..9d85d51f1bb1 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -42,6 +42,7 @@ + diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java index 71fa3201a4fe..8685950fc701 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java @@ -9,6 +9,7 @@ import de.tum.cit.aet.artemis.atlas.competency.util.PrerequisiteUtilService; import de.tum.cit.aet.artemis.atlas.competency.util.StandardizedCompetencyUtilService; import de.tum.cit.aet.artemis.atlas.learningpath.util.LearningPathUtilService; +import de.tum.cit.aet.artemis.atlas.profile.util.LearnerProfileUtilService; import de.tum.cit.aet.artemis.atlas.repository.CompetencyJolRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; @@ -145,6 +146,9 @@ public abstract class AbstractAtlasIntegrationTest extends AbstractSpringIntegra @Autowired protected LearningPathUtilService learningPathUtilService; + @Autowired + protected LearnerProfileUtilService learnerProfileUtilService; + // External Util Services @Autowired protected PageableSearchUtilService pageableSearchUtilService; diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java index a7bf7ca1aa01..9eca796775f4 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java @@ -83,6 +83,8 @@ void setupTestScenario() throws Exception { userUtilService.createAndSaveUser(TEST_PREFIX + "student1337"); userUtilService.createAndSaveUser(TEST_PREFIX + "instructor1337"); + learnerProfileUtilService.createLearnerProfilesForUsers(TEST_PREFIX); + course = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, true, true, 1).getFirst(); competencies = competencyUtilService.createCompetencies(course, 5); @@ -246,10 +248,11 @@ void testGenerateLearningPathOnEnrollment() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); request.postWithResponseBody("/api/courses/" + course.getId() + "/enroll", null, Set.class, HttpStatus.OK); - final var user = userTestRepository.findOneWithLearningPathsByLogin(TEST_PREFIX + "student1337").orElseThrow(); + final var user = userTestRepository.findOneWithLearningPathsAndLearnerProfileByLogin(TEST_PREFIX + "student1337").orElseThrow(); assertThat(user.getLearningPaths()).isNotNull(); - assertThat(user.getLearningPaths().size()).as("should create LearningPath for student").isEqualTo(1); + assertThat(user.getLearningPaths()).as("should create LearningPath for student").hasSize(1); + assertThat(user.getLearnerProfile().getCourseLearnerProfiles()).hasSize(1); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/profile/LearnerProfileIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/profile/LearnerProfileIntegrationTest.java new file mode 100644 index 000000000000..3eef9a88270a --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/profile/LearnerProfileIntegrationTest.java @@ -0,0 +1,38 @@ +package de.tum.cit.aet.artemis.atlas.profile; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.hibernate.Hibernate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.cit.aet.artemis.atlas.AbstractAtlasIntegrationTest; +import de.tum.cit.aet.artemis.core.domain.User; + +class LearnerProfileIntegrationTest extends AbstractAtlasIntegrationTest { + + private static final String TEST_PREFIX = "learnerprofiledatabase"; + + private static final int NUMBER_OF_STUDENTS = 1; + + private static final String STUDENT1_OF_COURSE = TEST_PREFIX + "student1"; + + @BeforeEach + void setupTestScenario() { + userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 1, 1, 1); + + // Add users that are not in the course + userUtilService.createAndSaveUser(TEST_PREFIX + "student1337"); + userUtilService.createAndSaveUser(TEST_PREFIX + "instructor1337"); + + learnerProfileUtilService.createLearnerProfilesForUsers(TEST_PREFIX); + } + + @Test + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") + void testLazyFetchLearnerProfile() { + User user = userTestRepository.getUserWithGroupsAndAuthorities(STUDENT1_OF_COURSE); + assertThat(Hibernate.isInitialized(user.getLearnerProfile())).isFalse(); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/profile/util/LearnerProfileUtilService.java b/src/test/java/de/tum/cit/aet/artemis/atlas/profile/util/LearnerProfileUtilService.java new file mode 100644 index 000000000000..bf1089a5ead6 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/profile/util/LearnerProfileUtilService.java @@ -0,0 +1,27 @@ +package de.tum.cit.aet.artemis.atlas.profile.util; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.atlas.domain.profile.LearnerProfile; +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; + +@Service +public class LearnerProfileUtilService { + + @Autowired + private UserTestRepository userTestRepository; + + public void createLearnerProfilesForUsers(String userPrefix) { + Set users = userTestRepository.findAllByUserPrefix(userPrefix).stream().peek(user -> { + LearnerProfile learnerProfile = new LearnerProfile(); + learnerProfile.setUser(user); + user.setLearnerProfile(learnerProfile); + }).collect(Collectors.toSet()); + userTestRepository.saveAll(users); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/core/test_repository/UserTestRepository.java b/src/test/java/de/tum/cit/aet/artemis/core/test_repository/UserTestRepository.java index 2e3623ba1e55..2b1734d80fb2 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/test_repository/UserTestRepository.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/test_repository/UserTestRepository.java @@ -59,8 +59,8 @@ default Page findAllWithGroupsByIsDeletedIsFalse(Pageable pageable) { return new PageImpl<>(users, pageable, total); } - @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) - Optional findOneWithLearningPathsByLogin(String login); + @EntityGraph(type = LOAD, attributePaths = { "learningPaths", "learnerProfile", "learnerProfile.courseLearnerProfiles" }) + Optional findOneWithLearningPathsAndLearnerProfileByLogin(String login); @EntityGraph(type = LOAD, attributePaths = { "learningPaths" }) Optional findWithLearningPathsById(long userId); @@ -75,4 +75,11 @@ default Page findAllWithGroupsByIsDeletedIsFalse(Pageable pageable) { default User findWithLearningPathsByIdElseThrow(long userId) { return getValueElseThrow(findWithLearningPathsById(userId), userId); } + + @Query(""" + SELECT user + FROM User user + WHERE user.login LIKE CONCAT(:userPrefix, '%') + """) + Set findAllByUserPrefix(String userPrefix); } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index 3f4606f09e63..763b15ec01a0 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -27,6 +27,7 @@ import de.tum.cit.aet.artemis.atlas.domain.science.ScienceEvent; import de.tum.cit.aet.artemis.atlas.domain.science.ScienceEventType; +import de.tum.cit.aet.artemis.atlas.repository.LearnerProfileRepository; import de.tum.cit.aet.artemis.atlas.test_repository.ScienceEventTestRepository; import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.Authority; @@ -109,6 +110,21 @@ public class UserTestService { @Autowired private MockMvc mockMvc; + @Autowired + private ParticipationVCSAccessTokenRepository participationVCSAccessTokenRepository; + + @Autowired + private ParticipationTestRepository participationRepository; + + @Autowired + private SubmissionTestRepository submissionRepository; + + @Autowired + private LearnerProfileRepository learnerProfileRepository; + + @Autowired + private ExerciseTestRepository exerciseTestRepository; + private String TEST_PREFIX; private MockDelegate mockDelegate; @@ -125,18 +141,6 @@ public class UserTestService { private static final int NUMBER_OF_INSTRUCTORS = 1; - @Autowired - private ParticipationVCSAccessTokenRepository participationVCSAccessTokenRepository; - - @Autowired - private ParticipationTestRepository participationRepository; - - @Autowired - private SubmissionTestRepository submissionRepository; - - @Autowired - private ExerciseTestRepository exerciseTestRepository; - public void setup(String testPrefix, MockDelegate mockDelegate) throws Exception { this.TEST_PREFIX = testPrefix; this.mockDelegate = mockDelegate; @@ -407,6 +411,8 @@ public User createExternalUser_asAdmin_isSuccessful() throws Exception { assertThat(student).as("New user is equal to request response").isEqualTo(response); assertThat(student).as("New user is equal to new user in DB").isEqualTo(userInDB); + assertThat(learnerProfileRepository.findByUser(student)).isNotEmpty(); + return userInDB; } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationJenkinsGitlabTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationJenkinsGitlabTest.java index 1923e7a53d54..292dbaf84e72 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationJenkinsGitlabTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationJenkinsGitlabTest.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import de.tum.cit.aet.artemis.atlas.profile.util.LearnerProfileUtilService; import de.tum.cit.aet.artemis.communication.repository.conversation.ChannelRepository; import de.tum.cit.aet.artemis.communication.test_repository.PostTestRepository; import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; @@ -202,4 +203,7 @@ public abstract class AbstractProgrammingIntegrationJenkinsGitlabTest extends Ab @Autowired protected UserUtilService userUtilService; + + @Autowired + protected LearnerProfileUtilService learnerProfileUtilService; } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java index b744f26b09c8..6d635786c9db 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java @@ -702,6 +702,7 @@ void testSearchStudentsAndTutorsAndInstructorsInOtherCourse_forbidden() throws E @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testAddStudentOrTutorOrInstructorToCourse() throws Exception { + learnerProfileUtilService.createLearnerProfilesForUsers(TEST_PREFIX); courseTestService.testAddStudentOrTutorOrEditorOrInstructorToCourse(); }