From 73b8614d83681406df7f02bea2082d77b94d0d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Thu, 7 Nov 2024 15:48:43 +0100 Subject: [PATCH 01/10] Implement aimForGradeOrBonus --- .../atlas/domain/profile/PreferenceScale.java | 16 ++++ .../repository/LearningPathRepository.java | 21 ++++-- .../LearningPathRecommendationService.java | 74 ++++++++++++++++--- .../learningpath/LearningPathService.java | 6 +- .../atlas/web/LearningPathResource.java | 8 +- .../core/repository/UserRepository.java | 24 ++++++ 6 files changed, 127 insertions(+), 22 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/PreferenceScale.java diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/PreferenceScale.java b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/PreferenceScale.java new file mode 100644 index 000000000000..d641c6849ffc --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/PreferenceScale.java @@ -0,0 +1,16 @@ +package de.tum.cit.aet.artemis.atlas.domain.profile; + +public enum PreferenceScale { + + LOW(1), MEDIUM_LOW(2), MEDIUM(3), MEDIUM_HIGH(4), HIGH(5); + + private final int value; + + PreferenceScale(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java index d07a63bcc3b9..78641a19458d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java @@ -63,11 +63,22 @@ SELECT COUNT (learningPath) """) long countLearningPathsOfEnrolledStudentsInCourse(@Param("courseId") long courseId); - @EntityGraph(type = LOAD, attributePaths = { "competencies", "competencies.lectureUnitLinks", "competencies.lectureUnitLinks.lectureUnit", "competencies.exerciseLinks", - "competencies.exerciseLinks.exercise" }) - Optional findWithCompetenciesAndLectureUnitsAndExercisesById(long learningPathId); + @Query(""" + SELECT l + FROM LearningPath l + LEFT JOIN FETCH l.competencies c + LEFT JOIN FETCH c.lectureUnitLinks lul + LEFT JOIN FETCH lul.lectureUnit + LEFT JOIN FETCH c.exerciseLinks el + LEFT JOIN FETCH el.exercise + LEFT JOIN FETCH l.user u + LEFT JOIN FETCH u.learnerProfile lp + LEFT JOIN FETCH lp.courseLearnerProfiles clp ON clp.course.id = l.course.id + WHERE lp.id = :learningPathId + """) + Optional findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfile(long learningPathId); - default LearningPath findWithCompetenciesAndLectureUnitsAndExercisesByIdElseThrow(long learningPathId) { - return getValueElseThrow(findWithCompetenciesAndLectureUnitsAndExercisesById(learningPathId), learningPathId); + default LearningPath findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileByIdElseThrow(long learningPathId) { + return getValueElseThrow(findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfile(learningPathId), learningPathId); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java index 1f3981baa4ce..48aae465fa99 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java @@ -1,6 +1,11 @@ package de.tum.cit.aet.artemis.atlas.service.learningpath; +import static de.tum.cit.aet.artemis.atlas.domain.profile.PreferenceScale.HIGH; +import static de.tum.cit.aet.artemis.atlas.domain.profile.PreferenceScale.LOW; +import static de.tum.cit.aet.artemis.atlas.domain.profile.PreferenceScale.MEDIUM_HIGH; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import static de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore.INCLUDED_AS_BONUS; +import static de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore.INCLUDED_COMPLETELY; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; @@ -14,6 +19,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -31,6 +37,7 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.LearningPath; import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite; import de.tum.cit.aet.artemis.atlas.domain.competency.RelationType; +import de.tum.cit.aet.artemis.atlas.domain.profile.CourseLearnerProfile; import de.tum.cit.aet.artemis.atlas.repository.CompetencyProgressRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; @@ -476,6 +483,9 @@ public List getRecommendedOrderOfLearningObjects(User user, Cour * @return the recommended ordering of learning objects */ public List getRecommendedOrderOfLearningObjects(User user, CourseCompetency competency, double combinedPriorConfidence) { + var learnerProfile = user.getLearnerProfile(); + var courseLearnerProfile = learnerProfile.getCourseLearnerProfiles().stream().findFirst().orElse(new CourseLearnerProfile()); + var pendingLectureUnits = competency.getLectureUnitLinks().stream().map(CompetencyLectureUnitLink::getLectureUnit).filter(lectureUnit -> !lectureUnit.isCompletedFor(user)) .toList(); List recommendedOrder = new ArrayList<>(pendingLectureUnits); @@ -504,7 +514,7 @@ public List getRecommendedOrderOfLearningObjects(User user, Cour } final var recommendedExerciseDistribution = getRecommendedExercisePointDistribution(numberOfRequiredExercisePointsToMaster, weightedConfidence); - scheduleExercisesByDistribution(recommendedOrder, recommendedExerciseDistribution, difficultyLevelMap); + scheduleExercisesByDistribution(recommendedOrder, recommendedExerciseDistribution, difficultyLevelMap, courseLearnerProfile); return recommendedOrder; } @@ -528,30 +538,30 @@ private void scheduleAllExercises(List recommendedOrder, Map recommendedOrder, double[] recommendedExercisePointDistribution, - Map> difficultyMap) { + Map> difficultyMap, CourseLearnerProfile courseLearnerProfile) { final var easyExercises = new ArrayList(); final var mediumExercises = new ArrayList(); final var hardExercises = new ArrayList(); // choose as many exercises from the correct difficulty level as possible - final var missingEasy = selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.EASY, recommendedExercisePointDistribution[0], easyExercises); - final var missingHard = selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.HARD, recommendedExercisePointDistribution[2], hardExercises); + final var missingEasy = selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.EASY, recommendedExercisePointDistribution[0], easyExercises, courseLearnerProfile); + final var missingHard = selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.HARD, recommendedExercisePointDistribution[2], hardExercises, courseLearnerProfile); // if there are not sufficiently many exercises per difficulty level, prefer medium difficulty // case 1: no medium exercises available/medium exercises missing: continue to fill with easy/hard exercises // case 2: medium exercises available: no medium exercises missing -> missing exercises must be easy/hard -> in both scenarios medium is the closest difficulty level double mediumExercisePoints = recommendedExercisePointDistribution[1] + missingEasy + missingHard; - double numberOfMissingExercisePoints = selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.MEDIUM, mediumExercisePoints, mediumExercises); + double numberOfMissingExercisePoints = selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.MEDIUM, mediumExercisePoints, mediumExercises, courseLearnerProfile); // if there are still not sufficiently many medium exercises, choose easy difficulty // prefer easy to hard exercises to avoid student overload if (numberOfMissingExercisePoints > 0 && !difficultyMap.get(DifficultyLevel.EASY).isEmpty()) { - numberOfMissingExercisePoints = selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.EASY, numberOfMissingExercisePoints, easyExercises); + numberOfMissingExercisePoints = selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.EASY, numberOfMissingExercisePoints, easyExercises, courseLearnerProfile); } // fill remaining slots with hard difficulty if (numberOfMissingExercisePoints > 0 && !difficultyMap.get(DifficultyLevel.HARD).isEmpty()) { - selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.HARD, numberOfMissingExercisePoints, hardExercises); + selectExercisesWithDifficulty(difficultyMap, DifficultyLevel.HARD, numberOfMissingExercisePoints, hardExercises, courseLearnerProfile); } recommendedOrder.addAll(easyExercises); @@ -571,15 +581,59 @@ private void scheduleExercisesByDistribution(List recommendedOrd * @return amount of points that are missing, if negative the amount of points that are selected too much */ private static double selectExercisesWithDifficulty(Map> difficultyMap, DifficultyLevel difficulty, double exercisePoints, - List exercises) { + List exercises, CourseLearnerProfile courseLearnerProfile) { var remainingExercisePoints = new AtomicDouble(exercisePoints); - var selectedExercises = difficultyMap.get(difficulty).stream().takeWhile(exercise -> remainingExercisePoints.getAndAdd(-exercise.getMaxPoints()) >= 0) - .collect(Collectors.toSet()); + + Comparator exerciseComparator = getExerciseOrderComparator(courseLearnerProfile.getAimForGradeOrBonus()); + Predicate exercisePredicate = getExerciseSelectionPredicate(courseLearnerProfile.getAimForGradeOrBonus(), remainingExercisePoints); + + var selectedExercises = difficultyMap.get(difficulty).stream().sorted(exerciseComparator).takeWhile(exercisePredicate).toList(); + exercises.addAll(selectedExercises); difficultyMap.get(difficulty).removeAll(selectedExercises); return remainingExercisePoints.get(); } + /** + * Creates a comparator that orders exercises based on the aim for grade or bonus. In case the student is at least medium low interested the comparator uses the inclusion in + * the score as sorting criterion. Otherwise, the order is unchanged + * + * @param aimForGradeOrBonus the aim for grade or bonus + * @return the comparator that orders the exercise based on the preference + */ + private static Comparator getExerciseOrderComparator(int aimForGradeOrBonus) { + if (aimForGradeOrBonus == LOW.getValue()) { + return Comparator.comparing(ignored -> 0); + } + else { + return Comparator.comparing(exercise -> switch (exercise.getIncludedInOverallScore()) { + case INCLUDED_COMPLETELY -> 0; + case INCLUDED_AS_BONUS -> 1; + case NOT_INCLUDED -> 2; + }); + } + } + + /** + * Creates a predicate that selects exercises based on the aim for grade or bonus and the remaining exercise points. + * + * @param aimForGradeOrBonus the aim for grade or bonus + * @param remainingExercisePoints the remaining exercise points that should be scheduled + * @return the predicate until when exercises should be selected based on the preference + */ + private static Predicate getExerciseSelectionPredicate(int aimForGradeOrBonus, AtomicDouble remainingExercisePoints) { + Predicate exercisePredicate = exercise -> remainingExercisePoints.getAndAdd(-exercise.getMaxPoints()) >= 0; + if (aimForGradeOrBonus == HIGH.getValue()) { + exercisePredicate = exercisePredicate + .and(exercise -> exercise.getIncludedInOverallScore() == INCLUDED_COMPLETELY || exercise.getIncludedInOverallScore() == INCLUDED_AS_BONUS); + } + else if (aimForGradeOrBonus == MEDIUM_HIGH.getValue()) { + exercisePredicate = exercisePredicate.and(exercise -> exercise.getIncludedInOverallScore() == INCLUDED_COMPLETELY); + } + + return exercisePredicate; + } + /** * Computes the average confidence of all prior competencies. * 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 c540b90ad3e3..faa2683ceed1 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 @@ -400,7 +400,7 @@ public LearningPathCompetencyGraphDTO generateLearningPathCompetencyInstructorGr * @return the navigation overview */ public LearningPathNavigationOverviewDTO getLearningPathNavigationOverview(long learningPathId) { - var learningPath = findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersById(learningPathId); + var learningPath = findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersAndLearnerProfileById(learningPathId); if (!userRepository.getUser().equals(learningPath.getUser())) { throw new AccessForbiddenException("You are not allowed to access this learning path"); } @@ -416,8 +416,8 @@ public LearningPathNavigationOverviewDTO getLearningPathNavigationOverview(long * @param learningPathId the id of the learning path to fetch * @return the learning path with fetched data */ - public LearningPath findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersById(long learningPathId) { - LearningPath learningPath = learningPathRepository.findWithCompetenciesAndLectureUnitsAndExercisesByIdElseThrow(learningPathId); + public LearningPath findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersAndLearnerProfileById(long learningPathId) { + LearningPath learningPath = learningPathRepository.findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileByIdElseThrow(learningPathId); // Remove exercises that are not visible to students learningPath.getCompetencies().forEach(competency -> competency diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java index 6bbdaaa3b91b..9ea5e2498bf3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java @@ -232,7 +232,7 @@ public ResponseEntity getRelativeLearningPathNavigati @RequestParam LearningObjectType learningObjectType, @RequestParam long competencyId) { log.debug("REST request to get navigation for learning path with id: {} relative to learning object with id: {} and type: {} in competency with id: {}", learningPathId, learningObjectId, learningObjectType, competencyId); - var learningPath = learningPathService.findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersById(learningPathId); + var learningPath = learningPathService.findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersAndLearnerProfileById(learningPathId); checkLearningPathAccessElseThrow(Optional.empty(), learningPath, Optional.empty()); return ResponseEntity.ok(learningPathNavigationService.getNavigationRelativeToLearningObject(learningPath, learningObjectId, learningObjectType, competencyId)); } @@ -248,7 +248,7 @@ public ResponseEntity getRelativeLearningPathNavigati @EnforceAtLeastStudent public ResponseEntity getLearningPathNavigation(@PathVariable long learningPathId) { log.debug("REST request to get navigation for learning path with id: {}", learningPathId); - var learningPath = learningPathService.findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersById(learningPathId); + var learningPath = learningPathService.findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersAndLearnerProfileById(learningPathId); checkLearningPathAccessElseThrow(Optional.empty(), learningPath, Optional.empty()); return ResponseEntity.ok(learningPathNavigationService.getNavigation(learningPath)); } @@ -341,7 +341,7 @@ public ResponseEntity> getCompetencyPr @EnforceAtLeastStudent public ResponseEntity> getCompetencyOrderForLearningPath(@PathVariable long learningPathId) { log.debug("REST request to get competency order for learning path: {}", learningPathId); - final var learningPath = learningPathService.findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersById(learningPathId); + final var learningPath = learningPathService.findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersAndLearnerProfileById(learningPathId); checkLearningPathAccessElseThrow(Optional.of(learningPath.getCourse()), learningPath, Optional.empty()); @@ -364,7 +364,7 @@ public ResponseEntity> getCompetencyOrderForLearningPath public ResponseEntity> getLearningObjectsForCompetency(@PathVariable long learningPathId, @PathVariable long competencyId) { log.debug("REST request to get learning objects for competency: {} in learning path: {}", competencyId, learningPathId); final var learningPath = learningPathRepository.findWithEagerCourseAndCompetenciesByIdElseThrow(learningPathId); - final var user = userRepository.getUserWithGroupsAndAuthorities(); + final var user = userRepository.getUserWithGroupsAndAuthoritiesAndLearnerProfile(learningPath.getCourse().getId()); checkLearningPathAccessElseThrow(Optional.of(learningPath.getCourse()), learningPath, Optional.of(user)); 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 286462286f68..1c6215cfdfe1 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 @@ -99,6 +99,18 @@ public interface UserRepository extends ArtemisJpaRepository, JpaSpe @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Optional findOneWithGroupsAndAuthoritiesByLogin(String login); + @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) + @Query(""" + SELECT u + FROM User u + LEFT JOIN FETCH u.groups + LEFT JOIN FETCH u.authorities + LEFT JOIN FETCH u.learnerProfile lp + LEFT JOIN FETCH lp.courseLearnerProfiles clp ON clp.course.id = :courseId + WHERE u.login = :login + """) + Optional findOneWithGroupsAndAuthoritiesAndLearnerProfileByLogin(@Param("login") String login, @Param("courseId") long courseId); + @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Optional findOneWithGroupsAndAuthoritiesByEmail(String email); @@ -901,6 +913,18 @@ default User getUserWithGroupsAndAuthorities() { return getValueElseThrow(findOneWithGroupsAndAuthoritiesByLogin(currentUserLogin)); } + /** + * Get user with user groups and authorities of currently logged-in user + * + * @param courseId the id of the course for which to load the user and the course learner profile + * @return currently logged-in user + */ + @NotNull + default User getUserWithGroupsAndAuthoritiesAndLearnerProfile(long courseId) { + String currentUserLogin = getCurrentUserLogin(); + return getValueElseThrow(findOneWithGroupsAndAuthoritiesAndLearnerProfileByLogin(currentUserLogin, courseId)); + } + /** * Get user with user groups, authorities and organizations of currently logged-in user * From bd77d2351cc78715831b80f65813339bd30ce000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Thu, 7 Nov 2024 17:09:39 +0100 Subject: [PATCH 02/10] Fix architecture --- .../atlas/repository/CourseLearnerProfileRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 9b97bdd798fc..ff9fa1de29b9 100644 --- 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 @@ -28,7 +28,7 @@ public interface CourseLearnerProfileRepository extends ArtemisJpaRepository Date: Sat, 9 Nov 2024 13:50:28 +0100 Subject: [PATCH 03/10] Fix exercise ordering --- .../LearningPathRecommendationService.java | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java index 48aae465fa99..2861f1b62434 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java @@ -1,7 +1,6 @@ package de.tum.cit.aet.artemis.atlas.service.learningpath; import static de.tum.cit.aet.artemis.atlas.domain.profile.PreferenceScale.HIGH; -import static de.tum.cit.aet.artemis.atlas.domain.profile.PreferenceScale.LOW; import static de.tum.cit.aet.artemis.atlas.domain.profile.PreferenceScale.MEDIUM_HIGH; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore.INCLUDED_AS_BONUS; @@ -46,6 +45,7 @@ import de.tum.cit.aet.artemis.exercise.domain.BaseExercise; import de.tum.cit.aet.artemis.exercise.domain.DifficultyLevel; import de.tum.cit.aet.artemis.exercise.domain.Exercise; +import de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore; import de.tum.cit.aet.artemis.lecture.domain.LectureUnit; import de.tum.cit.aet.artemis.lecture.service.LearningObjectService; @@ -105,6 +105,8 @@ public class LearningPathRecommendationService { private static final double[][] EXERCISE_DIFFICULTY_DISTRIBUTION_LUT = new double[][] { { 0.87, 0.12, 0.01 }, { 0.80, 0.18, 0.02 }, { 0.72, 0.25, 0.03 }, { 0.61, 0.33, 0.06 }, { 0.50, 0.40, 0.10 }, { 0.39, 0.45, 0.16 }, { 0.28, 0.48, 0.24 }, { 0.20, 0.47, 0.33 }, { 0.13, 0.43, 0.44 }, { 0.08, 0.37, 0.55 }, { 0.04, 0.29, 0.67 }, }; + private static final double COMPETENCY_LINK_WEIGHT_TO_GRADE_AIM_RATIO = 2; + protected LearningPathRecommendationService(CompetencyRelationRepository competencyRelationRepository, LearningObjectService learningObjectService, ParticipantScoreService participantScoreService, CompetencyProgressRepository competencyProgressRepository, CourseCompetencyRepository courseCompetencyRepository) { this.competencyRelationRepository = competencyRelationRepository; @@ -502,8 +504,7 @@ public List getRecommendedOrderOfLearningObjects(User user, Cour // First sort exercises based on title to ensure consistent ordering over multiple calls then prefer higher weighted exercises final var pendingExercises = competency.getExerciseLinks().stream().filter(link -> !learningObjectService.isCompletedByUser(link.getExercise(), user)) - .sorted(Comparator.comparing(link -> link.getExercise().getTitle())).sorted(Comparator.comparingDouble(CompetencyExerciseLink::getWeight).reversed()) - .map(CompetencyExerciseLink::getExercise).toList(); + .sorted(getExerciseOrderComparator(courseLearnerProfile.getAimForGradeOrBonus())).map(CompetencyExerciseLink::getExercise).toList(); final var pendingExercisePoints = pendingExercises.stream().mapToDouble(BaseExercise::getMaxPoints).sum(); @@ -584,34 +585,37 @@ private static double selectExercisesWithDifficulty(Map exercises, CourseLearnerProfile courseLearnerProfile) { var remainingExercisePoints = new AtomicDouble(exercisePoints); - Comparator exerciseComparator = getExerciseOrderComparator(courseLearnerProfile.getAimForGradeOrBonus()); Predicate exercisePredicate = getExerciseSelectionPredicate(courseLearnerProfile.getAimForGradeOrBonus(), remainingExercisePoints); - var selectedExercises = difficultyMap.get(difficulty).stream().sorted(exerciseComparator).takeWhile(exercisePredicate).toList(); + var selectedExercises = difficultyMap.get(difficulty).stream().takeWhile(exercisePredicate).toList(); exercises.addAll(selectedExercises); difficultyMap.get(difficulty).removeAll(selectedExercises); return remainingExercisePoints.get(); } + private static int getIncludeInOverallScoreWeight(IncludedInOverallScore includedInOverallScore) { + return switch (includedInOverallScore) { + case INCLUDED_COMPLETELY -> 0; + case INCLUDED_AS_BONUS -> 1; + case NOT_INCLUDED -> 2; + }; + } + /** - * Creates a comparator that orders exercises based on the aim for grade or bonus. In case the student is at least medium low interested the comparator uses the inclusion in - * the score as sorting criterion. Otherwise, the order is unchanged + * Creates a comparator that orders exercises based on the aim for grade or bonus, the link weight for the current competency and as a tiebreaker the lexicographic order of + * the exercise title. The higher the aim for the grade bonus is, the higher this metric is weighted compared to the link weight. * * @param aimForGradeOrBonus the aim for grade or bonus * @return the comparator that orders the exercise based on the preference */ - private static Comparator getExerciseOrderComparator(int aimForGradeOrBonus) { - if (aimForGradeOrBonus == LOW.getValue()) { - return Comparator.comparing(ignored -> 0); - } - else { - return Comparator.comparing(exercise -> switch (exercise.getIncludedInOverallScore()) { - case INCLUDED_COMPLETELY -> 0; - case INCLUDED_AS_BONUS -> 1; - case NOT_INCLUDED -> 2; - }); - } + private static Comparator getExerciseOrderComparator(int aimForGradeOrBonus) { + Comparator exerciseComparator = Comparator.comparingDouble(exerciseLink -> (COMPETENCY_LINK_WEIGHT_TO_GRADE_AIM_RATIO * exerciseLink.getWeight()) + + aimForGradeOrBonus * getIncludeInOverallScoreWeight(exerciseLink.getExercise().getIncludedInOverallScore())); + exerciseComparator = exerciseComparator.reversed(); + + exerciseComparator = exerciseComparator.thenComparing(exerciseLink -> exerciseLink.getExercise().getTitle()); + return exerciseComparator; } /** From 1eac74bb8ac2c5c822751b664dd30f505097820c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Mon, 11 Nov 2024 18:40:35 +0100 Subject: [PATCH 04/10] Fix server start up --- .../atlas/repository/LearningPathRepository.java | 14 +++++++++++--- .../service/learningpath/LearningPathService.java | 11 ++++++++++- .../artemis/core/repository/UserRepository.java | 3 ++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java index 78641a19458d..b49ce76baf04 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java @@ -36,6 +36,13 @@ default LearningPath findWithEagerUserByIdElseThrow(long learningPathId) { @EntityGraph(type = LOAD, attributePaths = { "competencies" }) Optional findWithEagerCompetenciesByCourseIdAndUserId(long courseId, long userId); + @EntityGraph(type = LOAD, attributePaths = { "course" }) + Optional findWithEagerCourseById(long learningPathId); + + default LearningPath findWithEagerCourseByIdElseThrow(long learningPathId) { + return getValueElseThrow(findWithEagerCourseById(learningPathId), learningPathId); + } + @EntityGraph(type = LOAD, attributePaths = { "course", "competencies" }) Optional findWithEagerCourseAndCompetenciesById(long learningPathId); @@ -73,12 +80,13 @@ SELECT COUNT (learningPath) LEFT JOIN FETCH el.exercise LEFT JOIN FETCH l.user u LEFT JOIN FETCH u.learnerProfile lp - LEFT JOIN FETCH lp.courseLearnerProfiles clp ON clp.course.id = l.course.id + LEFT JOIN FETCH lp.courseLearnerProfiles clp WHERE lp.id = :learningPathId + AND clp.course.id = l.course.id """) - Optional findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfile(long learningPathId); + Optional findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileById(long learningPathId); default LearningPath findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileByIdElseThrow(long learningPathId) { - return getValueElseThrow(findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfile(learningPathId), learningPathId); + return getValueElseThrow(findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileById(learningPathId), learningPathId); } } 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 2809a395ca67..c89ef2e28e75 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 @@ -418,7 +418,16 @@ public LearningPathNavigationOverviewDTO getLearningPathNavigationOverview(long * @return the learning path with fetched data */ public LearningPath findWithCompetenciesAndReleasedLearningObjectsAndCompletedUsersAndLearnerProfileById(long learningPathId) { - LearningPath learningPath = learningPathRepository.findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileByIdElseThrow(learningPathId); + Optional optionalLearningPath = learningPathRepository.findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileById(learningPathId); + LearningPath learningPath; + if (optionalLearningPath.isEmpty()) { + LearningPath learningPathWithCourse = learningPathRepository.findWithEagerCourseByIdElseThrow(learningPathId); + courseLearnerProfileService.createCourseLearnerProfile(learningPathWithCourse.getCourse(), learningPathWithCourse.getUser()); + learningPath = learningPathRepository.findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileByIdElseThrow(learningPathId); + } + else { + learningPath = optionalLearningPath.get(); + } // Remove exercises that are not visible to students learningPath.getCompetencies().forEach(competency -> competency 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 1c6215cfdfe1..43e2c44d9d6f 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 @@ -106,8 +106,9 @@ public interface UserRepository extends ArtemisJpaRepository, JpaSpe LEFT JOIN FETCH u.groups LEFT JOIN FETCH u.authorities LEFT JOIN FETCH u.learnerProfile lp - LEFT JOIN FETCH lp.courseLearnerProfiles clp ON clp.course.id = :courseId + LEFT JOIN FETCH lp.courseLearnerProfiles clp WHERE u.login = :login + AND clp.course.id = :courseId """) Optional findOneWithGroupsAndAuthoritiesAndLearnerProfileByLogin(@Param("login") String login, @Param("courseId") long courseId); From 6a37a21de91c9ce378911fefcb5dfe9a706e2be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Mon, 11 Nov 2024 20:40:25 +0100 Subject: [PATCH 05/10] Fix queries --- .../aet/artemis/atlas/repository/LearningPathRepository.java | 4 ++-- .../atlas/service/learningpath/LearningPathService.java | 1 + .../tum/cit/aet/artemis/core/repository/UserRepository.java | 1 - .../atlas/learningpath/LearningPathIntegrationTest.java | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java index b49ce76baf04..1bc5808d1307 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/LearningPathRepository.java @@ -81,10 +81,10 @@ SELECT COUNT (learningPath) LEFT JOIN FETCH l.user u LEFT JOIN FETCH u.learnerProfile lp LEFT JOIN FETCH lp.courseLearnerProfiles clp - WHERE lp.id = :learningPathId + WHERE l.id = :learningPathId AND clp.course.id = l.course.id """) - Optional findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileById(long learningPathId); + Optional findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileById(@Param("learningPathId") long learningPathId); default LearningPath findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileByIdElseThrow(long learningPathId) { return getValueElseThrow(findWithCompetenciesAndLectureUnitsAndExercisesAndLearnerProfileById(learningPathId), learningPathId); 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 c89ef2e28e75..39b20c08a139 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 @@ -132,6 +132,7 @@ public void enableLearningPathsForCourse(@NotNull Course course) { */ public void generateLearningPaths(@NotNull Course course) { var students = userRepository.getStudentsWithLearnerProfile(course); + courseLearnerProfileService.createCourseLearnerProfiles(course, students); generateLearningPaths(course, students); } 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 43e2c44d9d6f..29b83ff2ca08 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 @@ -99,7 +99,6 @@ public interface UserRepository extends ArtemisJpaRepository, JpaSpe @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Optional findOneWithGroupsAndAuthoritiesByLogin(String login); - @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) @Query(""" SELECT u FROM User u 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 e202856f4e94..827e91faea62 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 @@ -533,7 +533,7 @@ void testGetCompetencyProgressForLearningPathByInstructor() throws Exception { @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningPathNavigation() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); + final var student = userTestRepository.findOneWithGroupsAndAuthoritiesAndLearnerProfileByLogin(STUDENT1_OF_COURSE, course.getId()).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); competencyProgressService.updateProgressByLearningObjectSync(textUnit, Set.of(student)); @@ -632,7 +632,7 @@ void testGetRelativeLearningPathNavigation() throws Exception { } @Test - @WithMockUser(username = TEST_PREFIX + "student1337", roles = "USER") + @WithMockUser(username = STUDENT2_OF_COURSE, roles = "USER") void testGetLearningPathNavigationForOtherStudent() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); From eadd7bf41feb5af7486781baaa138aff773b70b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Mon, 11 Nov 2024 21:01:01 +0100 Subject: [PATCH 06/10] Fix tests --- .../LearningPathIntegrationTest.java | 6 ++-- .../service/LearningPathServiceTest.java | 31 +++---------------- 2 files changed, 6 insertions(+), 31 deletions(-) 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 827e91faea62..63e1f27d7c77 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 @@ -67,8 +67,6 @@ class LearningPathIntegrationTest extends AbstractAtlasIntegrationTest { private static final String STUDENT2_OF_COURSE = TEST_PREFIX + "student2"; - private static final String SECOND_STUDENT_OF_COURSE = TEST_PREFIX + "student2"; - private static final String TUTOR_OF_COURSE = TEST_PREFIX + "tutor1"; private static final String EDITOR_OF_COURSE = TEST_PREFIX + "editor1"; @@ -384,7 +382,7 @@ void testGetHealthStatusForCourse() throws Exception { @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningPathCompetencyGraphOfOtherUser() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); - final var otherStudent = userTestRepository.findOneByLogin(SECOND_STUDENT_OF_COURSE).orElseThrow(); + final var otherStudent = userTestRepository.findOneByLogin(STUDENT2_OF_COURSE).orElseThrow(); final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), otherStudent.getId()); request.get("/api/learning-path/" + learningPath.getId() + "/competency-graph", HttpStatus.FORBIDDEN, LearningPathCompetencyGraphDTO.class); } @@ -509,7 +507,7 @@ void shouldStartLearningPath() throws Exception { } @Test - @WithMockUser(username = SECOND_STUDENT_OF_COURSE, roles = "USER") + @WithMockUser(username = STUDENT2_OF_COURSE, roles = "USER") void testGetCompetencyProgressForLearningPathByOtherStudent() throws Exception { course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); final var student = userTestRepository.findOneByLogin(STUDENT1_OF_COURSE).orElseThrow(); diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java index d18a5ed65662..27c3bb70eedc 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java @@ -11,24 +11,17 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import de.tum.cit.aet.artemis.assessment.util.StudentScoreUtilService; -import de.tum.cit.aet.artemis.atlas.competency.util.CompetencyProgressUtilService; import de.tum.cit.aet.artemis.atlas.competency.util.CompetencyUtilService; import de.tum.cit.aet.artemis.atlas.domain.competency.RelationType; import de.tum.cit.aet.artemis.atlas.dto.LearningPathHealthDTO; import de.tum.cit.aet.artemis.atlas.learningpath.util.LearningPathUtilService; -import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathRecommendationService; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; 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.security.SecurityUtils; import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.core.util.CourseFactory; import de.tum.cit.aet.artemis.core.util.CourseUtilService; -import de.tum.cit.aet.artemis.lecture.repository.LectureUnitRepository; -import de.tum.cit.aet.artemis.lecture.util.LectureUtilService; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; class LearningPathServiceTest extends AbstractSpringIntegrationIndependentTest { @@ -50,28 +43,8 @@ class LearningPathServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private CompetencyUtilService competencyUtilService; - @Autowired - private LectureUtilService lectureUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private CompetencyRepository competencyRepository; - - @Autowired - private CompetencyProgressUtilService competencyProgressUtilService; - - @Autowired - private StudentScoreUtilService studentScoreUtilService; - - @Autowired - private LectureUnitRepository lectureUnitRepository; - private Course course; - private User user; - @BeforeEach void setAuthorizationForRepositoryRequests() { SecurityUtils.setAuthorizationObject(); @@ -88,9 +61,13 @@ class HealthCheck { @BeforeEach void setup() { userUtilService.addUsers(TEST_PREFIX, 5, 1, 1, 1); + + userUtilService.createLearnerProfilesForUsers(TEST_PREFIX); + course = CourseFactory.generateCourse(null, ZonedDateTime.now().minusDays(8), ZonedDateTime.now().minusDays(8), new HashSet<>(), TEST_PREFIX + "tumuser", TEST_PREFIX + "tutor", TEST_PREFIX + "editor", TEST_PREFIX + "instructor"); course = courseRepository.save(course); + } @Test From d174485549e7b9f6848aafb5655fc9d8e0307d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 19 Nov 2024 12:16:43 +0100 Subject: [PATCH 07/10] Fix test --- .../cit/aet/artemis/atlas/service/LearningPathServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java index 27c3bb70eedc..2d6570c9adf9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java @@ -107,7 +107,6 @@ class GenerateNgxPathRepresentation { @BeforeEach void setup() { final var users = userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 0); - user = users.getFirst(); course = CourseFactory.generateCourse(null, ZonedDateTime.now().minusDays(8), ZonedDateTime.now().minusDays(8), new HashSet<>(), TEST_PREFIX + "tumuser", TEST_PREFIX + "tutor", TEST_PREFIX + "editor", TEST_PREFIX + "instructor"); course = courseRepository.save(course); From d20b87e9e6ed0f01d957c454bca0f95c7c7b5b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Tue, 19 Nov 2024 14:12:08 +0100 Subject: [PATCH 08/10] Add some more tests --- .../atlas/AbstractAtlasIntegrationTest.java | 4 +++ .../LearningPathIntegrationTest.java | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+) 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..b176da78e1e6 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 @@ -13,6 +13,7 @@ import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; +import de.tum.cit.aet.artemis.atlas.repository.CourseLearnerProfileRepository; import de.tum.cit.aet.artemis.atlas.repository.KnowledgeAreaRepository; import de.tum.cit.aet.artemis.atlas.repository.ScienceSettingRepository; import de.tum.cit.aet.artemis.atlas.repository.SourceRepository; @@ -87,6 +88,9 @@ public abstract class AbstractAtlasIntegrationTest extends AbstractSpringIntegra @Autowired protected CompetencyLectureUnitLinkTestRepository competencyLectureUnitLinkRepository; + @Autowired + protected CourseLearnerProfileRepository courseLearnerProfileRepository; + // External Repositories @Autowired protected LectureRepository lectureRepository; 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 725036c30e2f..fa1a10569f30 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 @@ -9,6 +9,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.stream.IntStream; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -27,6 +28,7 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyRelation; import de.tum.cit.aet.artemis.atlas.domain.competency.LearningPath; import de.tum.cit.aet.artemis.atlas.domain.competency.RelationType; +import de.tum.cit.aet.artemis.atlas.domain.profile.CourseLearnerProfile; import de.tum.cit.aet.artemis.atlas.dto.CompetencyGraphNodeDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportOptionsDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyNameDTO; @@ -543,6 +545,40 @@ void testGetLearningPathNavigation() throws Exception { assertThat(result.progress()).isEqualTo(20); } + /** + * Provides all possible preferences for the course learner profile that can influence the navigation + * + * @return all possible combinations for a three tuple with the values between 0 and 5 (inclusive) + */ + static Stream getLearningPathNavigationPreferencesProvider() { + return IntStream.range(0, 6 * 6 * 6).mapToObj(i -> Arguments.of(i / 36, i / 6 % 6, i % 6)); + } + + @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") + @MethodSource("getLearningPathNavigationPreferencesProvider") + @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") + void testGetLearningPathNavigationPreferences(int aimForGradeOrBonus, int timeInvestment, int repetitionIntensity) throws Exception { + course = learningPathUtilService.enableAndGenerateLearningPathsForCourse(course); + + final var student = userTestRepository.findOneWithGroupsAndAuthoritiesAndLearnerProfileByLogin(STUDENT1_OF_COURSE, course.getId()).orElseThrow(); + CourseLearnerProfile learnerProfile = student.getLearnerProfile().getCourseLearnerProfiles().stream().filter(clp -> clp.getCourse().getId().equals(course.getId())) + .findFirst().orElseThrow(); + learnerProfile.setAimForGradeOrBonus(aimForGradeOrBonus); + learnerProfile.setTimeInvestment(timeInvestment); + learnerProfile.setRepetitionIntensity(repetitionIntensity); + courseLearnerProfileRepository.save(learnerProfile); + + createAndLinkTextUnit(student, competencies[2], false); + createAndLinkTextExercise(competencies[3], false); + + final var learningPath = learningPathRepository.findByCourseIdAndUserIdElseThrow(course.getId(), student.getId()); + final var result = request.get("/api/learning-path/" + learningPath.getId() + "/navigation", HttpStatus.OK, LearningPathNavigationDTO.class); + + assertThat(result.predecessorLearningObject().completed()).isTrue(); + assertThat(result.currentLearningObject().completed()).isFalse(); + assertThat(result.successorLearningObject().completed()).isFalse(); + } + @Test @WithMockUser(username = STUDENT1_OF_COURSE, roles = "USER") void testGetLearningPathNavigationEmptyCompetencies() throws Exception { From 6a5f18c034e693e922461ed0fe712e2b14bcb9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Thu, 21 Nov 2024 14:56:39 +0100 Subject: [PATCH 09/10] Flo --- .../cit/aet/artemis/atlas/domain/profile/PreferenceScale.java | 3 +++ .../learningpath/LearningPathRecommendationService.java | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/PreferenceScale.java b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/PreferenceScale.java index d641c6849ffc..d11012a396da 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/PreferenceScale.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/domain/profile/PreferenceScale.java @@ -1,5 +1,8 @@ package de.tum.cit.aet.artemis.atlas.domain.profile; +/** + * Enum for the preferences as lickert-scale regarding settings in the (course) learner profile, see {@link CourseLearnerProfile} and {@link LearnerProfile}. + */ public enum PreferenceScale { LOW(1), MEDIUM_LOW(2), MEDIUM(3), MEDIUM_HIGH(4), HIGH(5); diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java index 2861f1b62434..f4dfcb41f832 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/learningpath/LearningPathRecommendationService.java @@ -629,10 +629,10 @@ private static Predicate getExerciseSelectionPredicate(int aimForGrade Predicate exercisePredicate = exercise -> remainingExercisePoints.getAndAdd(-exercise.getMaxPoints()) >= 0; if (aimForGradeOrBonus == HIGH.getValue()) { exercisePredicate = exercisePredicate - .and(exercise -> exercise.getIncludedInOverallScore() == INCLUDED_COMPLETELY || exercise.getIncludedInOverallScore() == INCLUDED_AS_BONUS); + .or(exercise -> exercise.getIncludedInOverallScore() == INCLUDED_COMPLETELY || exercise.getIncludedInOverallScore() == INCLUDED_AS_BONUS); } else if (aimForGradeOrBonus == MEDIUM_HIGH.getValue()) { - exercisePredicate = exercisePredicate.and(exercise -> exercise.getIncludedInOverallScore() == INCLUDED_COMPLETELY); + exercisePredicate = exercisePredicate.or(exercise -> exercise.getIncludedInOverallScore() == INCLUDED_COMPLETELY); } return exercisePredicate; From 8ad593c16bcef317bccd1dede3f6a3c490b61b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= Date: Mon, 25 Nov 2024 15:00:21 +0100 Subject: [PATCH 10/10] Fix test compilation --- .../aet/artemis/atlas/service/LearningPathServiceTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java index 2d6570c9adf9..ac6a84831a01 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/service/LearningPathServiceTest.java @@ -15,6 +15,7 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.RelationType; import de.tum.cit.aet.artemis.atlas.dto.LearningPathHealthDTO; 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.service.learningpath.LearningPathRecommendationService; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; import de.tum.cit.aet.artemis.core.domain.Course; @@ -43,6 +44,9 @@ class LearningPathServiceTest extends AbstractSpringIntegrationIndependentTest { @Autowired private CompetencyUtilService competencyUtilService; + @Autowired + private LearnerProfileUtilService learnerProfileUtilService; + private Course course; @BeforeEach @@ -62,7 +66,7 @@ class HealthCheck { void setup() { userUtilService.addUsers(TEST_PREFIX, 5, 1, 1, 1); - userUtilService.createLearnerProfilesForUsers(TEST_PREFIX); + learnerProfileUtilService.createLearnerProfilesForUsers(TEST_PREFIX); course = CourseFactory.generateCourse(null, ZonedDateTime.now().minusDays(8), ZonedDateTime.now().minusDays(8), new HashSet<>(), TEST_PREFIX + "tumuser", TEST_PREFIX + "tutor", TEST_PREFIX + "editor", TEST_PREFIX + "instructor");