Skip to content

Commit

Permalink
Adaptive learning: Improve competency student view (#9916)
Browse files Browse the repository at this point in the history
  • Loading branch information
MaximilianAnzinger authored Dec 2, 2024
1 parent 8d70e37 commit ae08a1e
Show file tree
Hide file tree
Showing 10 changed files with 86 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -295,5 +295,14 @@ default CourseCompetency findByIdWithLectureUnitsAndExercisesElseThrow(long comp

List<CourseCompetency> 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<CourseCompetency> findByCourseIdAndLinkedToLearningObjectOrderById(@Param("courseId") long courseId);

boolean existsByIdAndCourseId(long competencyId, long courseId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CourseCompetency> findCourseCompetenciesWithProgressForUserByCourseId(Long courseId, Long userId) {
List<CourseCompetency> competencies = courseCompetencyRepository.findByCourseIdOrderById(courseId);
public List<CourseCompetency> findCourseCompetenciesWithProgressForUserByCourseId(long courseId, long userId, boolean filter) {
List<CourseCompetency> competencies;
if (filter) {
competencies = courseCompetencyRepository.findByCourseIdAndLinkedToLearningObjectOrderById(courseId);
}
else {
competencies = courseCompetencyRepository.findByCourseIdOrderById(courseId);
}
return findProgressForCompetenciesAndUser(competencies, userId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,10 @@ public ResponseEntity<CourseCompetency> getCourseCompetency(@PathVariable long c
*/
@GetMapping("courses/{courseId}/course-competencies")
@EnforceAtLeastStudentInCourse
public ResponseEntity<List<CourseCompetency>> getCourseCompetenciesWithProgress(@PathVariable long courseId) {
public ResponseEntity<List<CourseCompetency>> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ export class CourseCompetencyService {
.set('semester', pageable.semester);
}

getAllForCourse(courseId: number): Observable<EntityArrayResponseType> {
return this.httpClient.get<CourseCompetency[]>(`${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<EntityArrayResponseType> {
return this.httpClient
.get<CourseCompetency[]>(`${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) {
Expand Down
21 changes: 21 additions & 0 deletions src/main/webapp/app/entities/competency.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<? extends CourseCompetency> 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<? extends CourseCompetency> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ void shouldReturnCompetenciesForStudentOfCourse() throws Exception {
super.shouldReturnCompetenciesForCourse(new Competency());
}

@Override
List<? extends CourseCompetency> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,17 @@ void shouldReturnCompetenciesForStudentOfCourse() throws Exception {
super.shouldReturnCompetenciesForCourse(new Competency());
}

@Override
List<? extends CourseCompetency> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ void shouldReturnCompetenciesForStudentOfCourse() throws Exception {
super.shouldReturnCompetenciesForCourse(new Prerequisite());
}

@Override
List<? extends CourseCompetency> 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 {
Expand Down

0 comments on commit ae08a1e

Please sign in to comment.