From ae08a1edfda959887018c275b17d4b3139224998 Mon Sep 17 00:00:00 2001 From: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:55:38 +0100 Subject: [PATCH] Adaptive learning: Improve competency student view (#9916) --- .../CourseCompetencyRepository.java | 9 ++++++++ .../competency/CourseCompetencyService.java | 11 ++++++++-- .../atlas/web/CourseCompetencyResource.java | 4 ++-- .../competencies/course-competency.service.ts | 12 ++++++----- .../webapp/app/entities/competency.model.ts | 21 +++++++++++++++++++ .../course-competencies.component.ts | 6 +++--- ...CompetencyPrerequisiteIntegrationTest.java | 14 +++++++++++++ .../competency/CompetencyIntegrationTest.java | 5 +++++ .../CourseCompetencyIntegrationTest.java | 11 ++++++++++ .../PrerequisiteIntegrationTest.java | 5 +++++ 10 files changed, 86 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java index bba103f436d9..038d6bd63346 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/repository/CourseCompetencyRepository.java @@ -295,5 +295,14 @@ default CourseCompetency findByIdWithLectureUnitsAndExercisesElseThrow(long comp List findByCourseIdOrderById(long courseId); + @Query(""" + SELECT c + FROM CourseCompetency c + WHERE c.course.id = :courseId + AND (SIZE(c.lectureUnitLinks) > 0 OR SIZE(c.exerciseLinks) > 0) + ORDER BY c.id + """) + List findByCourseIdAndLinkedToLearningObjectOrderById(@Param("courseId") long courseId); + boolean existsByIdAndCourseId(long competencyId, long courseId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java index 88cad15f1000..77c1eda8385f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java @@ -124,10 +124,17 @@ public CourseCompetency findCompetencyWithExercisesAndLectureUnitsAndProgressFor * * @param courseId The id of the course for which to fetch the competencies * @param userId The id of the user for which to fetch the progress + * @param filter Whether to filter out competencies that are not linked to any learning objects * @return The found competency */ - public List findCourseCompetenciesWithProgressForUserByCourseId(Long courseId, Long userId) { - List competencies = courseCompetencyRepository.findByCourseIdOrderById(courseId); + public List findCourseCompetenciesWithProgressForUserByCourseId(long courseId, long userId, boolean filter) { + List competencies; + if (filter) { + competencies = courseCompetencyRepository.findByCourseIdAndLinkedToLearningObjectOrderById(courseId); + } + else { + competencies = courseCompetencyRepository.findByCourseIdOrderById(courseId); + } return findProgressForCompetenciesAndUser(competencies, userId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java index 8e93c6c73090..81183515dc38 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java @@ -167,10 +167,10 @@ public ResponseEntity getCourseCompetency(@PathVariable long c */ @GetMapping("courses/{courseId}/course-competencies") @EnforceAtLeastStudentInCourse - public ResponseEntity> getCourseCompetenciesWithProgress(@PathVariable long courseId) { + public ResponseEntity> getCourseCompetenciesWithProgress(@PathVariable long courseId, @RequestParam(defaultValue = "false") boolean filter) { log.debug("REST request to get competencies for course with id: {}", courseId); User user = userRepository.getUserWithGroupsAndAuthorities(); - final var competencies = courseCompetencyService.findCourseCompetenciesWithProgressForUserByCourseId(courseId, user.getId()); + final var competencies = courseCompetencyService.findCourseCompetenciesWithProgressForUserByCourseId(courseId, user.getId(), filter); return ResponseEntity.ok(competencies); } diff --git a/src/main/webapp/app/course/competencies/course-competency.service.ts b/src/main/webapp/app/course/competencies/course-competency.service.ts index e7a17d6d49ea..6e2f4c4046a8 100644 --- a/src/main/webapp/app/course/competencies/course-competency.service.ts +++ b/src/main/webapp/app/course/competencies/course-competency.service.ts @@ -74,11 +74,13 @@ export class CourseCompetencyService { .set('semester', pageable.semester); } - getAllForCourse(courseId: number): Observable { - return this.httpClient.get(`${this.resourceURL}/courses/${courseId}/course-competencies`, { observe: 'response' }).pipe( - map((res: EntityArrayResponseType) => this.convertArrayResponseDatesFromServer(res)), - tap((res: EntityArrayResponseType) => res?.body?.forEach(this.sendTitlesToEntityTitleService.bind(this))), - ); + getAllForCourse(courseId: number, filtered = false): Observable { + return this.httpClient + .get(`${this.resourceURL}/courses/${courseId}/course-competencies${filtered ? '?filter=true' : ''}`, { observe: 'response' }) + .pipe( + map((res: EntityArrayResponseType) => this.convertArrayResponseDatesFromServer(res)), + tap((res: EntityArrayResponseType) => res?.body?.forEach(this.sendTitlesToEntityTitleService.bind(this))), + ); } getProgress(competencyId: number, courseId: number, refresh = false) { diff --git a/src/main/webapp/app/entities/competency.model.ts b/src/main/webapp/app/entities/competency.model.ts index a1e01a1afb71..c130b3a24a67 100644 --- a/src/main/webapp/app/entities/competency.model.ts +++ b/src/main/webapp/app/entities/competency.model.ts @@ -287,3 +287,24 @@ export function getMastery(competencyProgress: CompetencyProgress | undefined): // clamp the value between 0 and 100 return Math.min(100, Math.max(0, Math.round(getProgress(competencyProgress) * getConfidence(competencyProgress)))); } + +/** + * Simple comparator for sorting competencies by their soft due date + * @param a The first competency + * @param b The second competency + */ +export function compareSoftDueDate(a: CourseCompetency, b: CourseCompetency): number { + if (a.softDueDate) { + if (b.softDueDate) { + if (a.softDueDate.isSame(b.softDueDate)) { + return 0; + } + return a.softDueDate.isBefore(b.softDueDate) ? -1 : 1; + } + return -1; + } + if (b.softDueDate) { + return 1; + } + return 0; +} diff --git a/src/main/webapp/app/overview/course-competencies/course-competencies.component.ts b/src/main/webapp/app/overview/course-competencies/course-competencies.component.ts index e6792fc21554..8d15b59fae94 100644 --- a/src/main/webapp/app/overview/course-competencies/course-competencies.component.ts +++ b/src/main/webapp/app/overview/course-competencies/course-competencies.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { AlertService } from 'app/core/util/alert.service'; import { onError } from 'app/shared/util/global.utils'; import { HttpErrorResponse } from '@angular/common/http'; -import { Competency, CompetencyJol, CourseCompetencyType, getMastery } from 'app/entities/competency.model'; +import { Competency, CompetencyJol, CourseCompetencyType, compareSoftDueDate, getMastery } from 'app/entities/competency.model'; import { Subscription, forkJoin, of } from 'rxjs'; import { Course } from 'app/entities/course.model'; import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons'; @@ -86,13 +86,13 @@ export class CourseCompetenciesComponent implements OnInit, OnDestroy { loadData() { this.isLoading = true; - const courseCompetencyObservable = this.courseCompetencyService.getAllForCourse(this.courseId); + const courseCompetencyObservable = this.courseCompetencyService.getAllForCourse(this.courseId, true); const competencyJolObservable = this.judgementOfLearningEnabled ? this.courseCompetencyService.getJoLAllForCourse(this.courseId) : of(undefined); forkJoin([courseCompetencyObservable, competencyJolObservable]).subscribe({ next: ([courseCompetencies, judgementOfLearningMap]) => { const courseCompetenciesResponse = courseCompetencies.body ?? []; - this.competencies = courseCompetenciesResponse.filter((competency) => competency.type === CourseCompetencyType.COMPETENCY); + this.competencies = courseCompetenciesResponse.filter((competency) => competency.type === CourseCompetencyType.COMPETENCY).sort(compareSoftDueDate); this.prerequisites = courseCompetenciesResponse.filter((competency) => competency.type === CourseCompetencyType.PREREQUISITE); if (judgementOfLearningMap !== undefined) { diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/AbstractCompetencyPrerequisiteIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/AbstractCompetencyPrerequisiteIntegrationTest.java index c3cf27caa54b..ea604fdfdf65 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/AbstractCompetencyPrerequisiteIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/AbstractCompetencyPrerequisiteIntegrationTest.java @@ -225,6 +225,20 @@ void shouldReturnCompetenciesForCourse(CourseCompetency newCompetency) throws Ex assertThat(competenciesOfCourse.stream().filter(l -> l.getId().equals(newCompetency.getId())).findFirst().orElseThrow().getLectureUnitLinks()).isEmpty(); } + abstract List getAllFilteredCall(long courseId, HttpStatus expectedStatus) throws Exception; + + // Test + void shouldReturnCompetenciesForCourseFiltered(CourseCompetency newCompetency) throws Exception { + newCompetency.setTitle("Title"); + newCompetency.setDescription("Description"); + newCompetency.setCourse(course); + courseCompetencyRepository.save(newCompetency); + + List competenciesOfCourse = getAllFilteredCall(course.getId(), HttpStatus.OK); + + assertThat(competenciesOfCourse).noneMatch(c -> c.getId().equals(newCompetency.getId())); + } + // Test void testShouldReturnForbiddenForStudentNotInCourse() throws Exception { getAllCall(course.getId(), HttpStatus.FORBIDDEN); diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CompetencyIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CompetencyIntegrationTest.java index 2f1ccc5c8536..bd32c7190f50 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CompetencyIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CompetencyIntegrationTest.java @@ -109,6 +109,11 @@ void shouldReturnCompetenciesForStudentOfCourse() throws Exception { super.shouldReturnCompetenciesForCourse(new Competency()); } + @Override + List getAllFilteredCall(long courseId, HttpStatus expectedStatus) throws Exception { + throw new UnsupportedOperationException("Not implemented for competencies"); + } + @Test @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") void testShouldReturnForbiddenForStudentNotInCourse() throws Exception { diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java index b12b09b6e191..e28c81456b18 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java @@ -152,6 +152,17 @@ void shouldReturnCompetenciesForStudentOfCourse() throws Exception { super.shouldReturnCompetenciesForCourse(new Competency()); } + @Override + List getAllFilteredCall(long courseId, HttpStatus expectedStatus) throws Exception { + return request.getList("/api/courses/" + courseId + "/course-competencies?filter=true", expectedStatus, CourseCompetency.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void shouldReturnCompetenciesForStudentOfCourseFiltered() throws Exception { + super.shouldReturnCompetenciesForCourseFiltered(new Competency()); + } + @Test @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") void testShouldReturnForbiddenForStudentNotInCourse() throws Exception { diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/PrerequisiteIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/PrerequisiteIntegrationTest.java index 9e84044aa6c9..ff0b8b916052 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/PrerequisiteIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/PrerequisiteIntegrationTest.java @@ -109,6 +109,11 @@ void shouldReturnCompetenciesForStudentOfCourse() throws Exception { super.shouldReturnCompetenciesForCourse(new Prerequisite()); } + @Override + List getAllFilteredCall(long courseId, HttpStatus expectedStatus) throws Exception { + throw new UnsupportedOperationException("Not implemented for prerequisites"); + } + @Test @WithMockUser(username = TEST_PREFIX + "student42", roles = "USER") void shouldReturnCompetenciesForStudentNotInCourse() throws Exception {