Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adaptive learning: Improve mastery calculation #8791

Merged
merged 37 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bd6edfd
Implement and fix tests
JohannesStoehr Jun 13, 2024
c2be757
Merge branch 'refs/heads/develop' into feature/adaptive-learning/bett…
JohannesStoehr Jun 13, 2024
5efea7b
Add tests
JohannesStoehr Jun 13, 2024
7869eb7
Fix tests
JohannesStoehr Jun 13, 2024
b687e46
Add Serverside for competency reason
JohannesStoehr Jun 14, 2024
b4fd8dc
Add java migration
JohannesStoehr Jun 14, 2024
a276b40
cleanup
JohannesStoehr Jun 14, 2024
f75f448
Fix logger
JohannesStoehr Jun 14, 2024
f6754e8
remove confidence, add ConfidenceReason
JohannesWt Jun 17, 2024
dfde61d
add hint text for confidence reason
JohannesWt Jun 17, 2024
6e7bd8b
Make Max happy
JohannesStoehr Jun 17, 2024
ff2422e
Finish client
JohannesStoehr Jun 17, 2024
8129cf4
Fix client tests
JohannesStoehr Jun 17, 2024
b53e1e2
Merge branch 'refs/heads/develop' into feature/adaptive-learning/bett…
JohannesStoehr Jun 17, 2024
6c46963
Improve translations
JohannesStoehr Jun 17, 2024
6d03eee
Fix Server Tests
JohannesStoehr Jun 17, 2024
3897b78
Coderabbit
JohannesStoehr Jun 18, 2024
cc6953c
Johannes
JohannesStoehr Jun 18, 2024
9b8bd5b
Merge branch 'refs/heads/develop' into feature/adaptive-learning/bett…
JohannesStoehr Jun 18, 2024
ef558c1
Fix rounding
JohannesStoehr Jun 18, 2024
ade59d5
Fix coverage
JohannesStoehr Jun 18, 2024
9be86da
Fix query for Postgres
JohannesStoehr Jun 18, 2024
40d6398
Small improvements
JohannesStoehr Jun 18, 2024
e022751
Fix translations
JohannesStoehr Jun 19, 2024
8cf94d6
Flo feedback 1
JohannesStoehr Jun 19, 2024
bafd411
Flo feedback 2
JohannesStoehr Jun 19, 2024
dc86e73
Merge branch 'refs/heads/develop' into feature/adaptive-learning/bett…
JohannesStoehr Jun 19, 2024
e389eed
Merge branch 'develop' into feature/adaptive-learning/better-mastery-…
JohannesStoehr Jun 19, 2024
2bec91b
Merge remote-tracking branch 'origin/feature/adaptive-learning/better…
JohannesStoehr Jun 19, 2024
e98d971
Add client test
JohannesStoehr Jun 20, 2024
f646922
Add explanation for expected values
JohannesStoehr Jun 20, 2024
7309bc7
Merge branch 'refs/heads/develop' into feature/adaptive-learning/bett…
JohannesStoehr Jun 22, 2024
021f0c2
Improve translation and add test
JohannesStoehr Jun 22, 2024
05cbcea
Merge branch 'refs/heads/develop' into feature/adaptive-learning/bett…
JohannesStoehr Jun 22, 2024
495ce59
Merge again
JohannesStoehr Jun 22, 2024
9b14d32
Fix client style
JohannesStoehr Jun 22, 2024
93ef487
Prettier
JohannesStoehr Jun 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/config/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,16 @@ public final class Constants {
*/
public static final String CHECKED_OUT_REPOS_TEMP_DIR = "checked-out-repos";

/**
* Minimum score for a result to be considered successful and shown in green
*/
public static final int MIN_SCORE_GREEN = 80;

/**
* Minimum score for a result to be considered partially successful and shown in orange
*/
public static final int MIN_SCORE_ORANGE = 40;

private Constants() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import de.tum.in.www1.artemis.config.migration.entries.MigrationEntry20240614_140000;

/**
* This component allows registering certain entries containing functionality that gets executed on application startup. The entries must extend {@link MigrationEntry}.
*/
Expand All @@ -26,6 +28,7 @@ public class MigrationRegistry {

public MigrationRegistry(MigrationService migrationService) {
this.migrationService = migrationService;
this.migrationEntryMap.put(1, MigrationEntry20240614_140000.class);
// Here we define the order of the ChangeEntries
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package de.tum.in.www1.artemis.config.migration.entries;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.tum.in.www1.artemis.config.migration.MigrationEntry;
import de.tum.in.www1.artemis.domain.Course;
import de.tum.in.www1.artemis.domain.competency.Competency;
import de.tum.in.www1.artemis.repository.CompetencyRepository;
import de.tum.in.www1.artemis.repository.CourseRepository;
import de.tum.in.www1.artemis.service.competency.CompetencyProgressService;

public class MigrationEntry20240614_140000 extends MigrationEntry {

private static final Logger log = LoggerFactory.getLogger(MigrationEntry20240614_140000.class);
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved

private final CourseRepository courseRepository;

private final CompetencyRepository competencyRepository;

private final CompetencyProgressService competencyProgressService;

public MigrationEntry20240614_140000(CourseRepository courseRepository, CompetencyRepository competencyRepository, CompetencyProgressService competencyProgressService) {
this.courseRepository = courseRepository;
this.competencyRepository = competencyRepository;
this.competencyProgressService = competencyProgressService;
}
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved

@Override
public void execute() {
List<Course> activeCourses = courseRepository.findAllActiveWithoutTestCourses(ZonedDateTime.now());

log.info("Updating competency progress for {} active courses", activeCourses.size());

activeCourses.forEach(course -> {
List<Competency> competencies = competencyRepository.findByCourseId(course.getId());
// Asynchronously update the progress for each competency
competencies.forEach(competencyProgressService::updateProgressByCompetencyAsync);
});
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public String author() {
return "stoehrj";
}

@Override
public String date() {
return "20240614_140000";
}
}
11 changes: 5 additions & 6 deletions src/main/java/de/tum/in/www1/artemis/domain/Exercise.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.tum.in.www1.artemis.domain;

import static de.tum.in.www1.artemis.config.Constants.MIN_SCORE_GREEN;
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -239,12 +241,9 @@ public abstract class Exercise extends BaseExercise implements LearningObject {

@Override
public boolean isCompletedFor(User user) {
return this.getStudentParticipations().stream().anyMatch((participation) -> participation.getStudents().contains(user));
}

@Override
public Optional<ZonedDateTime> getCompletionDate(User user) {
return this.getStudentParticipations().stream().filter((participation) -> participation.getStudents().contains(user)).map(Participation::getInitializationDate).findFirst();
var latestResult = this.getStudentParticipations().stream().filter(participation -> participation.getStudents().contains(user))
.flatMap(participation -> participation.getResults().stream()).max(Comparator.comparing(Result::getCompletionDate));
return latestResult.map(result -> result.getScore() >= MIN_SCORE_GREEN).orElse(false);
}

public boolean getAllowFeedbackRequests() {
Expand Down
10 changes: 0 additions & 10 deletions src/main/java/de/tum/in/www1/artemis/domain/LearningObject.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package de.tum.in.www1.artemis.domain;

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

import de.tum.in.www1.artemis.domain.competency.Competency;
Expand All @@ -16,14 +14,6 @@ public interface LearningObject {
*/
boolean isCompletedFor(User user);

/**
* Get the date when the object has been completed by the participant
*
* @param user the user to retrieve the date for
* @return The datetime when the object was first completed or null
*/
Optional<ZonedDateTime> getCompletionDate(User user);

Long getId();

Set<Competency> getCompetencies();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.Table;
Expand All @@ -23,6 +25,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.domain.enumeration.CompetencyProgressConfidenceReason;

/**
* This class models the 'progress' association between a user and a competency.
Expand Down Expand Up @@ -57,6 +60,10 @@ public class CompetencyProgress implements Serializable {
@Column(name = "confidence")
private Double confidence;

@Enumerated(EnumType.STRING)
@Column(name = "confidence_reason", columnDefinition = "varchar(30) default 'NO_REASON'")
private CompetencyProgressConfidenceReason confidenceReason = CompetencyProgressConfidenceReason.NO_REASON;

@LastModifiedDate
@Column(name = "last_modified_date")
@JsonIgnore
Expand Down Expand Up @@ -98,6 +105,14 @@ public void setConfidence(Double confidence) {
this.confidence = confidence;
}

public CompetencyProgressConfidenceReason getConfidenceReason() {
return confidenceReason;
}

public void setConfidenceReason(CompetencyProgressConfidenceReason confidenceReason) {
this.confidenceReason = confidenceReason;
}
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved

public Instant getLastModifiedDate() {
return lastModifiedDate;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package de.tum.in.www1.artemis.domain.enumeration;

import de.tum.in.www1.artemis.domain.competency.CompetencyProgress;

/**
* Enum to define the different reasons why the confidence is above/below 1 in the {@link CompetencyProgress}.
* A confidence != 1 leads to a higher/lower mastery, which is displayed to the student together with the reason.
* Also see {@link CompetencyProgress#setConfidenceReason}.
*/
public enum CompetencyProgressConfidenceReason {
NO_REASON, RECENT_SCORES_LOWER, RECENT_SCORES_HIGHER, MORE_EASY_POINTS, MORE_HARD_POINTS, QUICKLY_SOLVED_EXERCISES
}
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import java.time.ZonedDateTime;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import jakarta.persistence.CascadeType;
Expand Down Expand Up @@ -148,11 +147,6 @@ public boolean isCompletedFor(User user) {
return getCompletedUsers().stream().map(LectureUnitCompletion::getUser).anyMatch(user1 -> user1.getId().equals(user.getId()));
}

@Override
public Optional<ZonedDateTime> getCompletionDate(User user) {
return getCompletedUsers().stream().filter(completion -> completion.getUser().getId().equals(user.getId())).map(LectureUnitCompletion::getCompletedAt).findFirst();
}

// Used to distinguish the type when used in a DTO, e.g., LectureUnitForLearningPathNodeDetailsDTO.
public abstract String getType();
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,29 +72,20 @@ default CompetencyProgress findByCompetencyIdAndUserIdOrElseThrow(long competenc
""")
Set<CompetencyProgress> findAllByCompetencyIdsAndUserId(@Param("competencyIds") Set<Long> competencyIds, @Param("userId") long userId);

@Query("""
SELECT AVG(cp.confidence)
FROM CompetencyProgress cp
WHERE cp.competency.id = :competencyId
""")
Optional<Double> findAverageConfidenceByCompetencyId(@Param("competencyId") long competencyId);

@Query("""
SELECT COUNT(cp)
FROM CompetencyProgress cp
WHERE cp.competency.id = :competencyId
""")
Long countByCompetency(@Param("competencyId") long competencyId);
long countByCompetency(@Param("competencyId") long competencyId);

@Query("""
SELECT COUNT(cp)
FROM CompetencyProgress cp
WHERE cp.competency.id = :competencyId
AND cp.progress >= :progress
AND cp.confidence >= :confidence
AND cp.progress * cp.confidence >= :masteryThreshold
""")
Long countByCompetencyAndProgressAndConfidenceGreaterThanEqual(@Param("competencyId") long competencyId, @Param("progress") double progress,
@Param("confidence") double confidence);
long countByCompetencyAndMastered(@Param("competencyId") long competencyId, @Param("masteryThreshold") int masteryThreshold);
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved

@Query("""
SELECT cp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import de.tum.in.www1.artemis.domain.Course;
import de.tum.in.www1.artemis.domain.competency.Competency;
import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository;
import de.tum.in.www1.artemis.web.rest.dto.metrics.CompetencyExerciseMasteryCalculationDTO;
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;

/**
Expand Down Expand Up @@ -60,15 +61,42 @@ public interface CompetencyRepository extends ArtemisJpaRepository<Competency, L
""")
Optional<Competency> findByIdWithLectureUnitsAndCompletions(@Param("competencyId") long competencyId);

/**
* Fetches all information related to the calculation of the mastery for exercises in a competency.
* The complex grouping by is necessary for postgres
*
* @param competencyId the id of the competency for which to fetch the exercise information
* @return the exercise information for the calculation of the mastery in the competency
*/
@Query("""
SELECT new de.tum.in.www1.artemis.web.rest.dto.metrics.CompetencyExerciseMasteryCalculationDTO(
ex.maxPoints,
ex.difficulty,
CASE WHEN TYPE(ex) = ProgrammingExercise THEN TRUE ELSE FALSE END,
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved
COALESCE(sS.lastScore, tS.lastScore),
COALESCE(sS.lastPoints, tS.lastPoints),
COALESCE(sS.lastModifiedDate, tS.lastModifiedDate),
COUNT(s)
)
FROM Competency c
LEFT JOIN c.exercises ex
LEFT JOIN ex.studentParticipations sp
LEFT JOIN sp.submissions s
LEFT JOIN StudentScore sS ON sS.exercise = ex
LEFT JOIN TeamScore tS ON tS.exercise = ex
WHERE c.id = :competencyId
AND ex IS NOT NULL
GROUP BY ex.maxPoints, ex.difficulty, TYPE(ex), sS.lastScore, tS.lastScore, sS.lastPoints, tS.lastPoints, sS.lastModifiedDate, tS.lastModifiedDate
Comment on lines +72 to +89
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complexity increased due to Postgres not liking simpler queries. If someone else finds a way to retrieve the data in an easier way that also works on Postgres, I'll happily use it

""")
Set<CompetencyExerciseMasteryCalculationDTO> findAllExerciseInfoByCompetencyId(@Param("competencyId") long competencyId);
JohannesWt marked this conversation as resolved.
Show resolved Hide resolved
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved

@Query("""
SELECT c
FROM Competency c
LEFT JOIN FETCH c.exercises
LEFT JOIN FETCH c.lectureUnits lu
LEFT JOIN FETCH lu.completedUsers
LEFT JOIN FETCH c.exercises ex
WHERE c.id = :competencyId
""")
Optional<Competency> findByIdWithExercisesAndLectureUnitsAndCompletions(@Param("competencyId") long competencyId);
Optional<Competency> findByIdWithExercises(@Param("competencyId") long competencyId);

@Query("""
SELECT c
Expand Down Expand Up @@ -191,6 +219,10 @@ default Competency findByIdWithLectureUnitsAndCompletionsElseThrow(long competen
return findByIdWithLectureUnitsAndCompletions(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId));
}

default Competency findByIdWithExercisesElseThrow(long competencyId) {
return findByIdWithExercises(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId));
}

default Competency findByIdWithExercisesAndLectureUnitsBidirectionalElseThrow(long competencyId) {
return findByIdWithExercisesAndLectureUnitsBidirectional(competencyId).orElseThrow(() -> new EntityNotFoundException("Competency", competencyId));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,10 @@ public interface LectureUnitCompletionRepository extends ArtemisJpaRepository<Le
""")
Set<LectureUnitCompletion> findByLectureUnitsAndUserId(@Param("lectureUnits") Collection<? extends LectureUnit> lectureUnits, @Param("userId") Long userId);

@Query("""
SELECT COUNT(lectureUnitCompletion)
FROM LectureUnitCompletion lectureUnitCompletion
WHERE lectureUnitCompletion.lectureUnit.id IN :lectureUnitIds
""")
int countByLectureUnitIds(@Param("lectureUnitIds") Collection<Long> lectureUnitIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import de.tum.in.www1.artemis.config.Constants;
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.repository.base.ArtemisJpaRepository;
import de.tum.in.www1.artemis.web.rest.dto.metrics.ExerciseInformationDTO;
Expand Down Expand Up @@ -138,18 +139,20 @@ CASE WHEN TYPE(e) = ProgrammingExercise THEN TREAT(e AS ProgrammingExercise).all
*
* @param userId the id of the user
* @param exerciseIds the ids of the exercises
* @param minScore the minimum score required to consider an exercise as completed, normally {@link Constants#MIN_SCORE_GREEN }
* @return the ids of the completed exercises for the user in the exercises
*/
@Query("""
SELECT e.id
FROM Exercise e
LEFT JOIN e.studentParticipations p
LEFT JOIN e.teams t
LEFT JOIN t.students u
WHERE e.id IN :exerciseIds
AND (p.student.id = :userId OR u.id = :userId)
FROM ParticipantScore p
LEFT JOIN p.exercise e
LEFT JOIN TREAT (p AS StudentScore).user u
LEFT JOIN TREAT (p AS TeamScore).team.students s
WHERE (u.id = :userId OR s.id = :userId)
AND p.exercise.id IN :exerciseIds
AND COALESCE(p.lastRatedScore, p.lastScore, 0) >= :minScore
""")
Set<Long> findAllCompletedExerciseIdsForUserByExerciseIds(@Param("userId") long userId, @Param("exerciseIds") Set<Long> exerciseIds);
Set<Long> findAllCompletedExerciseIdsForUserByExerciseIds(@Param("userId") long userId, @Param("exerciseIds") Set<Long> exerciseIds, @Param("minScore") double minScore);

/**
* Get the ids of the teams the user is in for a set of exercises.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.in.www1.artemis.service;

import static de.tum.in.www1.artemis.config.Constants.MIN_SCORE_GREEN;
import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE;

import java.util.Set;
Expand All @@ -13,7 +14,6 @@
import de.tum.in.www1.artemis.domain.LearningObject;
import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.domain.lecture.LectureUnit;
import de.tum.in.www1.artemis.domain.lecture.LectureUnitCompletion;

/**
* Service implementation for interactions with learning objects.
Expand Down Expand Up @@ -42,12 +42,10 @@ public LearningObjectService(ParticipantScoreService participantScoreService) {
* @return true if the user completed the lecture unit or has at least one result for the exercise, false otherwise
*/
public boolean isCompletedByUser(@NotNull LearningObject learningObject, @NotNull User user) {
if (learningObject instanceof LectureUnit lectureUnit) {
return lectureUnit.getCompletedUsers().stream().map(LectureUnitCompletion::getUser).anyMatch(user1 -> user1.getId().equals(user.getId()));
}
else if (learningObject instanceof Exercise exercise) {
return participantScoreService.getStudentAndTeamParticipations(user, Set.of(exercise)).findAny().isPresent();
}
throw new IllegalArgumentException("Learning object must be either LectureUnit or Exercise");
return switch (learningObject) {
case LectureUnit lectureUnit -> lectureUnit.isCompletedFor(user);
case Exercise exercise -> participantScoreService.getStudentAndTeamParticipations(user, Set.of(exercise)).anyMatch(score -> score.getLastScore() >= MIN_SCORE_GREEN);
default -> throw new IllegalArgumentException("Learning object must be either LectureUnit or Exercise");
};
JohannesStoehr marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading
Loading