-
Notifications
You must be signed in to change notification settings - Fork 303
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adaptive learning
: Add endpoint to retrieve student course metrics (#…
…8508) Co-authored-by: Johannes Stöhr <[email protected]>
- Loading branch information
1 parent
2f0dee7
commit c5a5c28
Showing
18 changed files
with
594 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
src/main/java/de/tum/in/www1/artemis/repository/metrics/ExerciseMetricsRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package de.tum.in.www1.artemis.repository.metrics; | ||
|
||
import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; | ||
|
||
import java.util.Set; | ||
|
||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.data.jpa.repository.Query; | ||
import org.springframework.data.repository.query.Param; | ||
import org.springframework.stereotype.Repository; | ||
|
||
import de.tum.in.www1.artemis.domain.Exercise; | ||
import de.tum.in.www1.artemis.web.rest.dto.metrics.ExerciseInformationDTO; | ||
import de.tum.in.www1.artemis.web.rest.dto.metrics.ResourceTimestampDTO; | ||
import de.tum.in.www1.artemis.web.rest.dto.metrics.ScoreDTO; | ||
|
||
/** | ||
* Spring Data JPA repository to fetch exercise related metrics. | ||
*/ | ||
@Profile(PROFILE_CORE) | ||
@Repository | ||
public interface ExerciseMetricsRepository extends JpaRepository<Exercise, Long> { | ||
|
||
/** | ||
* Get the exercise information for all exercises in a course. | ||
* | ||
* @param courseId the id of the course | ||
* @return the exercise information for all exercises in the course | ||
*/ | ||
@Query(""" | ||
SELECT new de.tum.in.www1.artemis.web.rest.dto.metrics.ExerciseInformationDTO(e.id, e.shortName, e.title, COALESCE(e.startDate, e.releaseDate), e.dueDate, e.maxPoints, e.class) | ||
FROM Exercise e | ||
WHERE e.course.id = :courseId | ||
""") | ||
Set<ExerciseInformationDTO> findAllExerciseInformationByCourseId(@Param("courseId") long courseId); | ||
|
||
@Query(""" | ||
SELECT new de.tum.in.www1.artemis.web.rest.dto.metrics.ScoreDTO(p.exercise.id, AVG(COALESCE(p.lastScore, 0))) | ||
FROM ParticipantScore p | ||
WHERE p.exercise.id IN :exerciseIds | ||
GROUP BY p.exercise.id | ||
""") | ||
Set<ScoreDTO> findAverageScore(@Param("exerciseIds") Set<Long> exerciseIds); | ||
|
||
@Query(""" | ||
SELECT new de.tum.in.www1.artemis.web.rest.dto.metrics.ScoreDTO(s.exercise.id, CAST(COALESCE(s.lastRatedScore, s.lastScore, 0) AS DOUBLE)) | ||
FROM StudentScore s | ||
WHERE s.exercise.id IN :exerciseIds | ||
AND s.user.id = :userId | ||
""") | ||
Set<ScoreDTO> findScore(@Param("exerciseIds") Set<Long> exerciseIds, @Param("userId") long userId); | ||
|
||
/** | ||
* Get the latest submission dates for a user in a set of exercises. | ||
* | ||
* @param exerciseIds the ids of the exercises | ||
* @param userId the id of the user | ||
* @return the latest submission dates for the user in the exercises | ||
*/ | ||
@Query(""" | ||
SELECT new de.tum.in.www1.artemis.web.rest.dto.metrics.ResourceTimestampDTO(e.id, s.submissionDate) | ||
FROM Submission s | ||
LEFT JOIN StudentParticipation p ON s.participation.id = p.id | ||
LEFT JOIN p.exercise e | ||
LEFT JOIN p.team t | ||
LEFT JOIN t.students u | ||
WHERE e.id IN :exerciseIds | ||
AND s.submissionDate = ( | ||
SELECT MAX(s2.submissionDate) | ||
FROM Submission s2 | ||
WHERE s2.participation.id = s.participation.id | ||
AND s2.submitted = TRUE | ||
) | ||
AND (p.student.id = :userId OR u.id = :userId) | ||
""") | ||
Set<ResourceTimestampDTO> findLatestSubmissionDatesForUser(@Param("exerciseIds") Set<Long> exerciseIds, @Param("userId") long userId); | ||
|
||
/** | ||
* Get the latest submission dates for a set of exercises. | ||
* | ||
* @param exerciseIds the ids of the exercises | ||
* @return the latest submission dates for the exercises | ||
*/ | ||
@Query(""" | ||
SELECT new de.tum.in.www1.artemis.web.rest.dto.metrics.ResourceTimestampDTO(e.id, s.submissionDate) | ||
FROM Submission s | ||
LEFT JOIN StudentParticipation p ON s.participation.id = p.id | ||
LEFT JOIN p.exercise e | ||
WHERE e.id IN :exerciseIds | ||
AND s.submissionDate = ( | ||
SELECT MAX(s2.submissionDate) | ||
FROM Submission s2 | ||
WHERE s2.participation.id = s.participation.id | ||
AND s2.submitted = TRUE | ||
) | ||
""") | ||
Set<ResourceTimestampDTO> findLatestSubmissionDates(@Param("exerciseIds") Set<Long> exerciseIds); | ||
} |
80 changes: 80 additions & 0 deletions
80
src/main/java/de/tum/in/www1/artemis/service/metrics/MetricsService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package de.tum.in.www1.artemis.service.metrics; | ||
|
||
import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; | ||
import static de.tum.in.www1.artemis.service.util.ZonedDateTimeUtil.toRelativeTime; | ||
import static java.util.function.Function.identity; | ||
import static java.util.stream.Collectors.averagingDouble; | ||
import static java.util.stream.Collectors.groupingBy; | ||
import static java.util.stream.Collectors.toMap; | ||
|
||
import java.time.ZonedDateTime; | ||
import java.util.function.Predicate; | ||
import java.util.function.ToDoubleFunction; | ||
|
||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.stereotype.Service; | ||
|
||
import de.tum.in.www1.artemis.repository.metrics.ExerciseMetricsRepository; | ||
import de.tum.in.www1.artemis.web.rest.dto.metrics.ExerciseInformationDTO; | ||
import de.tum.in.www1.artemis.web.rest.dto.metrics.ExerciseStudentMetricsDTO; | ||
import de.tum.in.www1.artemis.web.rest.dto.metrics.ResourceTimestampDTO; | ||
import de.tum.in.www1.artemis.web.rest.dto.metrics.ScoreDTO; | ||
import de.tum.in.www1.artemis.web.rest.dto.metrics.StudentMetricsDTO; | ||
|
||
/** | ||
* Service class to access metrics regarding students' learning progress. | ||
*/ | ||
@Profile(PROFILE_CORE) | ||
@Service | ||
public class MetricsService { | ||
|
||
private final ExerciseMetricsRepository exerciseMetricsRepository; | ||
|
||
public MetricsService(ExerciseMetricsRepository exerciseMetricsRepository) { | ||
this.exerciseMetricsRepository = exerciseMetricsRepository; | ||
} | ||
|
||
/** | ||
* Get the metrics for a student in a course. | ||
* | ||
* @param userId the id of the student | ||
* @param courseId the id of the course | ||
* @return the metrics for the student in the course | ||
*/ | ||
public StudentMetricsDTO getStudentCourseMetrics(long userId, long courseId) { | ||
final var exerciseMetricsDTO = getStudentExerciseMetrics(userId, courseId); | ||
return new StudentMetricsDTO(exerciseMetricsDTO); | ||
} | ||
|
||
/** | ||
* Get the exercise metrics for a student in a course. | ||
* | ||
* @param userId the id of the student | ||
* @param courseId the id of the course | ||
* @return the metrics for the student in the course | ||
*/ | ||
public ExerciseStudentMetricsDTO getStudentExerciseMetrics(long userId, long courseId) { | ||
final var exerciseInfo = exerciseMetricsRepository.findAllExerciseInformationByCourseId(courseId); | ||
// generate map and remove exercises that are not yet started | ||
final Predicate<ExerciseInformationDTO> started = e -> e.start() != null && e.start().isBefore(ZonedDateTime.now()); | ||
final var exerciseInfoMap = exerciseInfo.stream().filter(started).collect(toMap(ExerciseInformationDTO::id, identity())); | ||
|
||
final var exerciseIds = exerciseInfoMap.keySet(); | ||
|
||
final var averageScore = exerciseMetricsRepository.findAverageScore(exerciseIds); | ||
final var averageScoreMap = averageScore.stream().collect(toMap(ScoreDTO::exerciseId, ScoreDTO::score)); | ||
|
||
final var score = exerciseMetricsRepository.findScore(exerciseIds, userId); | ||
final var scoreMap = score.stream().collect(toMap(ScoreDTO::exerciseId, ScoreDTO::score)); | ||
|
||
final var latestSubmissions = exerciseMetricsRepository.findLatestSubmissionDates(exerciseIds); | ||
final ToDoubleFunction<ResourceTimestampDTO> relativeTime = dto -> toRelativeTime(exerciseInfoMap.get(dto.id()).start(), exerciseInfoMap.get(dto.id()).due(), | ||
dto.timestamp()); | ||
final var averageLatestSubmissionMap = latestSubmissions.stream().collect(groupingBy(ResourceTimestampDTO::id, averagingDouble(relativeTime))); | ||
|
||
final var latestSubmissionOfUser = exerciseMetricsRepository.findLatestSubmissionDatesForUser(exerciseIds, userId); | ||
final var latestSubmissionMap = latestSubmissionOfUser.stream().collect(toMap(ResourceTimestampDTO::id, relativeTime::applyAsDouble)); | ||
|
||
return new ExerciseStudentMetricsDTO(exerciseInfoMap, averageScoreMap, scoreMap, averageLatestSubmissionMap, latestSubmissionMap); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
src/main/java/de/tum/in/www1/artemis/service/util/ZonedDateTimeUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package de.tum.in.www1.artemis.service.util; | ||
|
||
import java.time.ZonedDateTime; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
|
||
public class ZonedDateTimeUtil { | ||
|
||
/** | ||
* Private constructor to prevent instantiation. | ||
*/ | ||
private ZonedDateTimeUtil() { | ||
throw new IllegalStateException("Utility class"); | ||
} | ||
|
||
/** | ||
* Get the relative time of a ZonedDateTime object compared to an origin and a unit ZonedDateTime object in percent. | ||
* <p> | ||
* Example: origin = 0:00, unit = 10:00, target = 2:30 => 25% | ||
* | ||
* @param origin the origin ZonedDateTime object | ||
* @param unit the unit ZonedDateTime object | ||
* @param target the target ZonedDateTime object | ||
* @return the relative time of the target ZonedDateTime object compared to the origin and unit ZonedDateTime objects | ||
*/ | ||
public static double toRelativeTime(@NotNull ZonedDateTime origin, @NotNull ZonedDateTime unit, @NotNull ZonedDateTime target) { | ||
return 100.0 * (target.toEpochSecond() - origin.toEpochSecond()) / (unit.toEpochSecond() - origin.toEpochSecond()); | ||
} | ||
} |
49 changes: 49 additions & 0 deletions
49
src/main/java/de/tum/in/www1/artemis/web/rest/MetricsResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package de.tum.in.www1.artemis.web.rest; | ||
|
||
import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PathVariable; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import de.tum.in.www1.artemis.repository.UserRepository; | ||
import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastStudentInCourse; | ||
import de.tum.in.www1.artemis.service.metrics.MetricsService; | ||
import de.tum.in.www1.artemis.web.rest.dto.metrics.StudentMetricsDTO; | ||
|
||
@Profile(PROFILE_CORE) | ||
@RestController | ||
@RequestMapping("api/metrics/") | ||
public class MetricsResource { | ||
|
||
private static final Logger log = LoggerFactory.getLogger(MetricsResource.class); | ||
|
||
private final MetricsService metricsService; | ||
|
||
private final UserRepository userRepository; | ||
|
||
public MetricsResource(MetricsService metricsService, UserRepository userRepository) { | ||
this.metricsService = metricsService; | ||
this.userRepository = userRepository; | ||
} | ||
|
||
/** | ||
* GET course/:courseId/student : Gets the metrics of a course for the logged-in user. | ||
* | ||
* @param courseId the id of the course from which to get the metrics | ||
* @return the ResponseEntity with status 200 (OK) with body the student metrics for the course | ||
*/ | ||
@GetMapping("course/{courseId}/student") | ||
@EnforceAtLeastStudentInCourse | ||
public ResponseEntity<StudentMetricsDTO> getCourseMetricsForUser(@PathVariable long courseId) { | ||
final var userId = userRepository.getUserIdElseThrow(); // won't throw exception since EnforceRoleInResource checks existence of user | ||
log.debug("REST request to get the metrics for the user with id {} in the course with id {}", userId, courseId); | ||
final var studentMetrics = metricsService.getStudentCourseMetrics(userId, courseId); | ||
return ResponseEntity.ok(studentMetrics); | ||
} | ||
} |
Oops, something went wrong.